mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Osquery] Add ECS mapping editor (#107706)
This commit is contained in:
parent
0828788b66
commit
c347a7e5e5
25 changed files with 1065 additions and 102 deletions
|
@ -309,6 +309,7 @@
|
|||
"nock": "12.0.3",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-forge": "^0.10.0",
|
||||
"node-sql-parser": "^3.6.1",
|
||||
"nodemailer": "^6.6.2",
|
||||
"normalize-path": "^3.0.0",
|
||||
"object-hash": "^1.3.1",
|
||||
|
|
|
@ -73,6 +73,7 @@ export const DEV_ONLY_LICENSE_ALLOWED = ['MPL-2.0'];
|
|||
export const LICENSE_OVERRIDES = {
|
||||
'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts
|
||||
'@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint
|
||||
'node-sql-parser@3.6.1': ['(GPL-2.0 OR MIT)'], // GPL-2.0* https://github.com/taozhi8833998/node-sql-parser
|
||||
'@elastic/ems-client@7.14.0': ['Elastic License 2.0'],
|
||||
'@elastic/eui@36.0.0': ['SSPL-1.0 OR Elastic License 2.0'],
|
||||
|
||||
|
|
|
@ -44,6 +44,16 @@ export interface OsqueryManagerPackagePolicyConfigRecord {
|
|||
interval: OsqueryManagerPackagePolicyConfigRecordEntry;
|
||||
platform?: OsqueryManagerPackagePolicyConfigRecordEntry;
|
||||
version?: OsqueryManagerPackagePolicyConfigRecordEntry;
|
||||
ecs_mapping?:
|
||||
| {
|
||||
value: Record<
|
||||
string,
|
||||
{
|
||||
field: string;
|
||||
}
|
||||
>;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export interface OsqueryManagerPackagePolicyInputStream
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
useUiSetting$,
|
||||
withKibana,
|
||||
reactRouterNavigate,
|
||||
FieldIcon,
|
||||
} from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { StartServices } from '../../../types';
|
||||
|
||||
|
@ -47,4 +48,5 @@ export {
|
|||
useUiSetting,
|
||||
useUiSetting$,
|
||||
withKibana,
|
||||
FieldIcon,
|
||||
};
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { EuiCodeEditor } from '@elastic/eui';
|
||||
import 'brace/theme/tomorrow';
|
||||
|
||||
|
@ -22,27 +22,34 @@ const EDITOR_PROPS = {
|
|||
|
||||
interface OsqueryEditorProps {
|
||||
defaultValue: string;
|
||||
disabled?: boolean;
|
||||
onChange: (newValue: string) => void;
|
||||
}
|
||||
|
||||
const OsqueryEditorComponent: React.FC<OsqueryEditorProps> = ({
|
||||
defaultValue,
|
||||
// disabled,
|
||||
onChange,
|
||||
}) => (
|
||||
<EuiCodeEditor
|
||||
value={defaultValue}
|
||||
mode="osquery"
|
||||
// isReadOnly={disabled}
|
||||
theme="tomorrow"
|
||||
onChange={onChange}
|
||||
name="osquery_editor"
|
||||
setOptions={EDITOR_SET_OPTIONS}
|
||||
editorProps={EDITOR_PROPS}
|
||||
height="150px"
|
||||
width="100%"
|
||||
/>
|
||||
);
|
||||
const OsqueryEditorComponent: React.FC<OsqueryEditorProps> = ({ defaultValue, onChange }) => {
|
||||
const editorValue = useRef(defaultValue ?? '');
|
||||
|
||||
const handleChange = useCallback((newValue: string) => {
|
||||
editorValue.current = newValue;
|
||||
}, []);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
onChange(editorValue.current.replaceAll('\n', ' ').replaceAll(' ', ' '));
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<EuiCodeEditor
|
||||
onBlur={onBlur}
|
||||
value={defaultValue}
|
||||
mode="osquery"
|
||||
onChange={handleChange}
|
||||
theme="tomorrow"
|
||||
name="osquery_editor"
|
||||
setOptions={EDITOR_SET_OPTIONS}
|
||||
editorProps={EDITOR_PROPS}
|
||||
height="150px"
|
||||
width="100%"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const OsqueryEditor = React.memo(OsqueryEditorComponent);
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -20,11 +20,8 @@ let osqueryTables: TablesJSON | null = null;
|
|||
export const getOsqueryTables = () => {
|
||||
if (!osqueryTables) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
osqueryTables = normalizeTables(require('./osquery_schema/v4.8.0.json'));
|
||||
osqueryTables = normalizeTables(require('../common/schemas/osquery/v4.9.0.json'));
|
||||
}
|
||||
return osqueryTables;
|
||||
};
|
||||
export const getOsqueryTableNames = () =>
|
||||
flatMap(getOsqueryTables(), (table) => {
|
||||
return table.name;
|
||||
});
|
||||
export const getOsqueryTableNames = () => flatMap(getOsqueryTables(), (table) => table.name);
|
||||
|
|
|
@ -58,7 +58,7 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({ disa
|
|||
/>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow fullWidth labelAppend={<OsquerySchemaLink />}>
|
||||
{!permissions.writeLiveQueries ? (
|
||||
{!permissions.writeLiveQueries || disabled ? (
|
||||
<StyledEuiCodeBlock
|
||||
language="sql"
|
||||
fontSize="m"
|
||||
|
@ -68,7 +68,7 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({ disa
|
|||
{value}
|
||||
</StyledEuiCodeBlock>
|
||||
) : (
|
||||
<OsqueryEditor defaultValue={value} disabled={disabled} onChange={handleEditorChange} />
|
||||
<OsqueryEditor defaultValue={value} onChange={handleEditorChange} />
|
||||
)}
|
||||
</EuiFormRow>
|
||||
</>
|
||||
|
|
|
@ -35,6 +35,12 @@ interface GetNewStreamProps {
|
|||
platform?: string | undefined;
|
||||
version?: string | undefined;
|
||||
scheduledQueryGroupId?: string;
|
||||
ecs_mapping?: Record<
|
||||
string,
|
||||
{
|
||||
field: string;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
interface GetNewStreamReturn extends Omit<OsqueryManagerPackagePolicyInputStream, 'id'> {
|
||||
|
@ -65,6 +71,11 @@ const getNewStream = (payload: GetNewStreamProps) =>
|
|||
if (payload.version && draft.vars) {
|
||||
draft.vars.version = { type: 'text', value: payload.version };
|
||||
}
|
||||
if (payload.ecs_mapping && draft.vars) {
|
||||
draft.vars.ecs_mapping = {
|
||||
value: payload.ecs_mapping,
|
||||
};
|
||||
}
|
||||
return draft;
|
||||
}
|
||||
);
|
||||
|
@ -146,6 +157,14 @@ const QueriesFieldComponent: React.FC<QueriesFieldProps> = ({
|
|||
delete draft[0].streams[showEditQueryFlyout].vars.version;
|
||||
}
|
||||
|
||||
if (updatedQuery.ecs_mapping) {
|
||||
draft[0].streams[showEditQueryFlyout].vars.ecs_mapping = {
|
||||
value: updatedQuery.ecs_mapping,
|
||||
};
|
||||
} else {
|
||||
delete draft[0].streams[showEditQueryFlyout].vars.ecs_mapping;
|
||||
}
|
||||
|
||||
return draft;
|
||||
})
|
||||
);
|
||||
|
|
|
@ -0,0 +1,829 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { produce } from 'immer';
|
||||
import { find, sortBy, isArray, map } from 'lodash';
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useImperativeHandle,
|
||||
MutableRefObject,
|
||||
} from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiComboBox,
|
||||
EuiComboBoxProps,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
import { Parser, Select } from 'node-sql-parser';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import deepmerge from 'deepmerge';
|
||||
|
||||
import ECSSchema from '../../common/schemas/ecs/v1.11.0.json';
|
||||
import osquerySchema from '../../common/schemas/osquery/v4.9.0.json';
|
||||
|
||||
import { FieldIcon } from '../../common/lib/kibana';
|
||||
import {
|
||||
FIELD_TYPES,
|
||||
Form,
|
||||
FormData,
|
||||
FieldHook,
|
||||
getFieldValidityAndErrorMessage,
|
||||
useForm,
|
||||
useFormData,
|
||||
Field,
|
||||
getUseField,
|
||||
fieldValidators,
|
||||
ValidationFuncArg,
|
||||
} from '../../shared_imports';
|
||||
|
||||
export const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
const typeMap = {
|
||||
binary: 'binary',
|
||||
half_float: 'number',
|
||||
scaled_float: 'number',
|
||||
float: 'number',
|
||||
integer: 'number',
|
||||
long: 'number',
|
||||
short: 'number',
|
||||
byte: 'number',
|
||||
text: 'string',
|
||||
keyword: 'string',
|
||||
'': 'string',
|
||||
geo_point: 'geo_point',
|
||||
date: 'date',
|
||||
ip: 'ip',
|
||||
boolean: 'boolean',
|
||||
constant_keyword: 'string',
|
||||
};
|
||||
|
||||
const StyledFieldIcon = styled(FieldIcon)`
|
||||
width: 32px;
|
||||
padding: 0 4px;
|
||||
`;
|
||||
|
||||
const StyledFieldSpan = styled.span`
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
`;
|
||||
|
||||
// align the icon to the inputs
|
||||
const StyledButtonWrapper = styled.div`
|
||||
margin-top: 32px;
|
||||
`;
|
||||
|
||||
const singleSelection = { asPlainText: true };
|
||||
|
||||
const ECSSchemaOptions = ECSSchema.map((ecs) => ({
|
||||
label: ecs.field,
|
||||
value: ecs,
|
||||
}));
|
||||
|
||||
type ECSSchemaOption = typeof ECSSchemaOptions[0];
|
||||
|
||||
interface ECSComboboxFieldProps {
|
||||
field: FieldHook<string>;
|
||||
euiFieldProps: EuiComboBoxProps<ECSSchemaOption>;
|
||||
idAria?: string;
|
||||
}
|
||||
|
||||
export const ECSComboboxField: React.FC<ECSComboboxFieldProps> = ({
|
||||
field,
|
||||
euiFieldProps = {},
|
||||
idAria,
|
||||
...rest
|
||||
}) => {
|
||||
const { setValue } = field;
|
||||
const [selectedOptions, setSelected] = useState<Array<EuiComboBoxOptionOption<ECSSchemaOption>>>(
|
||||
[]
|
||||
);
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
const describedByIds = useMemo(() => (idAria ? [idAria] : []), [idAria]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newSelectedOptions) => {
|
||||
setSelected(newSelectedOptions);
|
||||
setValue(newSelectedOptions[0]?.label ?? '');
|
||||
},
|
||||
[setSelected, setValue]
|
||||
);
|
||||
|
||||
// TODO: Create own component for this.
|
||||
const renderOption = useCallback(
|
||||
(option, searchValue, contentClassName) => (
|
||||
<EuiFlexGroup
|
||||
className={`${contentClassName} euiSuggestItem`}
|
||||
alignItems="center"
|
||||
gutterSize="xs"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
<FieldIcon type={typeMap[option.value.type] ?? option.value.type} />
|
||||
}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledFieldSpan className="euiSuggestItem__label euiSuggestItem__labelDisplay--expand">
|
||||
{option.value.field}
|
||||
</StyledFieldSpan>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<span className="euiSuggestItem__description euiSuggestItem__description--truncate">
|
||||
{option.value.description}
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const prepend = useMemo(
|
||||
() => (
|
||||
<StyledFieldIcon
|
||||
size="l"
|
||||
type={
|
||||
// @ts-expect-error update types
|
||||
selectedOptions[0]?.value?.type === 'keyword' ? 'string' : selectedOptions[0]?.value?.type
|
||||
}
|
||||
/>
|
||||
),
|
||||
[selectedOptions]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-expect-error update types
|
||||
setSelected(() => {
|
||||
if (!field.value.length) return [];
|
||||
|
||||
const selectedOption = find(ECSSchemaOptions, ['label', field.value]);
|
||||
|
||||
return selectedOption ? [selectedOption] : [];
|
||||
});
|
||||
}, [field.value]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
helpText={typeof field.helpText === 'function' ? field.helpText() : field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
describedByIds={describedByIds}
|
||||
{...rest}
|
||||
>
|
||||
<EuiComboBox
|
||||
prepend={prepend}
|
||||
fullWidth
|
||||
singleSelection={singleSelection}
|
||||
// @ts-expect-error update types
|
||||
options={ECSSchemaOptions}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={handleChange}
|
||||
renderOption={renderOption}
|
||||
rowHeight={32}
|
||||
isClearable
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
interface OsqueryColumnFieldProps {
|
||||
field: FieldHook<string>;
|
||||
euiFieldProps: EuiComboBoxProps<OsquerySchemaOption>;
|
||||
idAria?: string;
|
||||
}
|
||||
|
||||
export const OsqueryColumnField: React.FC<OsqueryColumnFieldProps> = ({
|
||||
field,
|
||||
euiFieldProps = {},
|
||||
idAria,
|
||||
...rest
|
||||
}) => {
|
||||
const { setErrors, setValue } = field;
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
const describedByIds = useMemo(() => (idAria ? [idAria] : []), [idAria]);
|
||||
const [selectedOptions, setSelected] = useState<
|
||||
Array<EuiComboBoxOptionOption<OsquerySchemaOption>>
|
||||
>([]);
|
||||
|
||||
const renderOsqueryOption = useCallback(
|
||||
(option, searchValue, contentClassName) => (
|
||||
<EuiFlexGroup
|
||||
className={`${contentClassName} euiSuggestItem`}
|
||||
alignItems="center"
|
||||
gutterSize="xs"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledFieldSpan className="euiSuggestItem__label euiSuggestItem__labelDisplay--expand">
|
||||
{option.value.suggestion_label}
|
||||
</StyledFieldSpan>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<span className="euiSuggestItem__description euiSuggestItem__description--truncate">
|
||||
{option.value.description}
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const onCreateOsqueryOption = useCallback(
|
||||
(searchValue = []) => {
|
||||
const normalizedSearchValue = searchValue.trim().toLowerCase();
|
||||
|
||||
if (!normalizedSearchValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOption = {
|
||||
label: searchValue,
|
||||
};
|
||||
|
||||
// Select the option.
|
||||
setSelected([newOption]);
|
||||
setValue(newOption.label);
|
||||
},
|
||||
[setValue, setSelected]
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newSelectedOptions) => {
|
||||
setSelected(newSelectedOptions);
|
||||
setValue(newSelectedOptions[0]?.label ?? '');
|
||||
},
|
||||
[setValue, setSelected]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(() => {
|
||||
if (!field.value.length) return [];
|
||||
|
||||
const selectedOption = find(euiFieldProps?.options, ['label', field.value]);
|
||||
|
||||
return selectedOption ? [selectedOption] : [{ label: field.value }];
|
||||
});
|
||||
}, [euiFieldProps?.options, setSelected, field.value, setErrors]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
helpText={typeof field.helpText === 'function' ? field.helpText() : field.helpText}
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth
|
||||
describedByIds={describedByIds}
|
||||
{...rest}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
singleSelection={singleSelection}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={handleChange}
|
||||
onCreateOption={onCreateOsqueryOption}
|
||||
renderOption={renderOsqueryOption}
|
||||
rowHeight={32}
|
||||
isClearable
|
||||
{...euiFieldProps}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ECSMappingEditorFieldRef {
|
||||
validate: () => Promise<
|
||||
| Record<
|
||||
string,
|
||||
{
|
||||
field: string;
|
||||
}
|
||||
>
|
||||
| false
|
||||
| {}
|
||||
>;
|
||||
}
|
||||
|
||||
export interface ECSMappingEditorFieldProps {
|
||||
field: FieldHook<string>;
|
||||
query: string;
|
||||
fieldRef: MutableRefObject<ECSMappingEditorFieldRef>;
|
||||
}
|
||||
|
||||
interface ECSMappingEditorFormProps {
|
||||
osquerySchemaOptions: OsquerySchemaOption[];
|
||||
defaultValue?: FormData;
|
||||
onAdd?: (payload: FormData) => void;
|
||||
onChange?: (payload: FormData) => void;
|
||||
onDelete?: (key: string) => void;
|
||||
}
|
||||
|
||||
const getEcsFieldValidator = (editForm: boolean) => (
|
||||
args: ValidationFuncArg<ECSMappingEditorFormData, ECSMappingEditorFormData['key']>
|
||||
) => {
|
||||
const fieldRequiredError = fieldValidators.emptyField(
|
||||
i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.ecsFieldRequiredErrorMessage',
|
||||
{
|
||||
defaultMessage: 'ECS field is required.',
|
||||
}
|
||||
)
|
||||
)(args);
|
||||
|
||||
if (fieldRequiredError && (!!(!editForm && args.formData.value?.field.length) || editForm)) {
|
||||
return fieldRequiredError;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getOsqueryResultFieldValidator = (
|
||||
osquerySchemaOptions: OsquerySchemaOption[],
|
||||
editForm: boolean
|
||||
) => (
|
||||
args: ValidationFuncArg<ECSMappingEditorFormData, ECSMappingEditorFormData['value']['field']>
|
||||
) => {
|
||||
const fieldRequiredError = fieldValidators.emptyField(
|
||||
i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.osqueryResultFieldRequiredErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Osquery result is required.',
|
||||
}
|
||||
)
|
||||
)(args);
|
||||
|
||||
if (fieldRequiredError && ((!editForm && args.formData.key.length) || editForm)) {
|
||||
return fieldRequiredError;
|
||||
}
|
||||
|
||||
if (!args.value.length) return;
|
||||
|
||||
const osqueryColumnExists = find(osquerySchemaOptions, ['label', args.value]);
|
||||
|
||||
return !osqueryColumnExists
|
||||
? {
|
||||
code: 'ERR_FIELD_FORMAT',
|
||||
path: args.path,
|
||||
message: i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.osqueryResultFieldValueMissingErrorMessage',
|
||||
{
|
||||
defaultMessage: 'The current query does not return a {columnName} field',
|
||||
values: {
|
||||
columnName: args.value,
|
||||
},
|
||||
}
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const FORM_DEFAULT_VALUE = {
|
||||
key: '',
|
||||
value: { field: '' },
|
||||
};
|
||||
|
||||
interface ECSMappingEditorFormData {
|
||||
key: string;
|
||||
value: {
|
||||
field: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ECSMappingEditorFormRef {
|
||||
validate: () => Promise<{
|
||||
data: ECSMappingEditorFormData | {};
|
||||
isValid: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const ECSMappingEditorForm = forwardRef<ECSMappingEditorFormRef, ECSMappingEditorFormProps>(
|
||||
({ osquerySchemaOptions, defaultValue, onAdd, onChange, onDelete }, ref) => {
|
||||
const editForm = !!defaultValue;
|
||||
const currentFormData = useRef(defaultValue);
|
||||
const formSchema = {
|
||||
key: {
|
||||
label: i18n.translate('xpack.osquery.scheduledQueryGroup.queryFlyoutForm.ecsFieldLabel', {
|
||||
defaultMessage: 'ECS field',
|
||||
}),
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
fieldsToValidateOnChange: ['value.field'],
|
||||
validations: [
|
||||
{
|
||||
validator: getEcsFieldValidator(editForm),
|
||||
},
|
||||
],
|
||||
},
|
||||
'value.field': {
|
||||
label: i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.osqueryResultFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Osquery result',
|
||||
}
|
||||
),
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
fieldsToValidateOnChange: ['key'],
|
||||
validations: [
|
||||
{
|
||||
validator: getOsqueryResultFieldValidator(osquerySchemaOptions, editForm),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const { form } = useForm({
|
||||
schema: formSchema,
|
||||
defaultValue: defaultValue ?? FORM_DEFAULT_VALUE,
|
||||
});
|
||||
|
||||
const { submit, reset, validate, __validateFields } = form;
|
||||
|
||||
const [formData] = useFormData({ form });
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
validate();
|
||||
__validateFields(['value.field']);
|
||||
const { data, isValid } = await submit();
|
||||
|
||||
if (isValid) {
|
||||
if (onAdd) {
|
||||
onAdd(data);
|
||||
}
|
||||
reset();
|
||||
}
|
||||
return { data, isValid };
|
||||
}, [validate, __validateFields, submit, onAdd, reset]);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
if (defaultValue?.key && onDelete) {
|
||||
onDelete(defaultValue.key);
|
||||
}
|
||||
}, [defaultValue, onDelete]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
validate: async () => {
|
||||
if (!editForm && deepEqual(formData, FORM_DEFAULT_VALUE)) {
|
||||
return { data: {}, isValid: true };
|
||||
}
|
||||
|
||||
__validateFields(['value.field']);
|
||||
const isValid = await validate();
|
||||
|
||||
return { data: formData?.key?.length ? { [formData.key]: formData.value } : {}, isValid };
|
||||
},
|
||||
}),
|
||||
[__validateFields, editForm, formData, validate]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange && !deepEqual(formData, currentFormData.current)) {
|
||||
currentFormData.current = formData;
|
||||
onChange(formData);
|
||||
}
|
||||
}, [defaultValue, formData, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue) {
|
||||
validate();
|
||||
__validateFields(['value.field']);
|
||||
}
|
||||
}, [defaultValue, osquerySchemaOptions, validate, __validateFields]);
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<EuiFlexGroup alignItems="flexStart">
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="value.field"
|
||||
component={OsqueryColumnField}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{
|
||||
label: 'Osquery result',
|
||||
options: osquerySchemaOptions,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledButtonWrapper>
|
||||
<EuiIcon type="arrowRight" />
|
||||
</StyledButtonWrapper>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField path="key" component={ECSComboboxField} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledButtonWrapper>
|
||||
{defaultValue ? (
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.deleteECSMappingRowButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Delete ECS mapping row',
|
||||
}
|
||||
)}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
onClick={handleDeleteClick}
|
||||
/>
|
||||
) : (
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.osquery.scheduledQueryGroup.queryFlyoutForm.addECSMappingRowButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Add ECS mapping row',
|
||||
}
|
||||
)}
|
||||
iconType="plus"
|
||||
color="primary"
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
)}
|
||||
</StyledButtonWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface OsquerySchemaOption {
|
||||
label: string;
|
||||
value: {
|
||||
name: string;
|
||||
description: string;
|
||||
table: string;
|
||||
suggestion_label: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface OsqueryColumn {
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
hidden: boolean;
|
||||
required: boolean;
|
||||
index: boolean;
|
||||
}
|
||||
|
||||
export const ECSMappingEditorField = ({ field, query, fieldRef }: ECSMappingEditorFieldProps) => {
|
||||
const { setValue, value = {} } = field;
|
||||
const [osquerySchemaOptions, setOsquerySchemaOptions] = useState<OsquerySchemaOption[]>([]);
|
||||
const formRefs = useRef<Record<string, ECSMappingEditorFormRef>>({});
|
||||
|
||||
useImperativeHandle(
|
||||
fieldRef,
|
||||
() => ({
|
||||
validate: async () => {
|
||||
const validations = await Promise.all(
|
||||
Object.values(formRefs.current).map(async (formRef) => {
|
||||
const { data, isValid } = await formRef.validate();
|
||||
return [data, isValid];
|
||||
})
|
||||
);
|
||||
|
||||
if (find(validations, (result) => result[1] === false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return deepmerge.all(map(validations, '[0]'));
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setOsquerySchemaOptions((currentValue) => {
|
||||
if (!query?.length) {
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
const parser = new Parser();
|
||||
let ast: Select;
|
||||
|
||||
try {
|
||||
const parsedQuery = parser.astify(query);
|
||||
ast = (isArray(parsedQuery) ? parsedQuery[0] : parsedQuery) as Select;
|
||||
} catch (e) {
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
const astOsqueryTables: Record<string, OsqueryColumn[]> = ast?.from?.reduce((acc, table) => {
|
||||
const osqueryTable = find(osquerySchema, ['name', table.table]);
|
||||
|
||||
if (osqueryTable) {
|
||||
acc[table.as ?? table.table] = osqueryTable.columns;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Table doesn't exist in osquery schema
|
||||
if (
|
||||
!isArray(ast?.columns) &&
|
||||
ast?.columns !== '*' &&
|
||||
!astOsqueryTables[ast?.from && ast?.from[0].table]
|
||||
) {
|
||||
return currentValue;
|
||||
}
|
||||
/*
|
||||
Simple query
|
||||
select * from users;
|
||||
*/
|
||||
if (ast?.columns === '*' && ast.from?.length && astOsqueryTables[ast.from[0].table]) {
|
||||
const tableName = ast.from[0].as ?? ast.from[0].table;
|
||||
|
||||
return astOsqueryTables[ast.from[0].table].map((osqueryColumn) => ({
|
||||
label: osqueryColumn.name,
|
||||
value: {
|
||||
name: osqueryColumn.name,
|
||||
description: osqueryColumn.description,
|
||||
table: tableName,
|
||||
suggestion_label: osqueryColumn.name,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
/*
|
||||
Advanced query
|
||||
select i.*, p.resident_size, p.user_time, p.system_time, time.minutes as counter from osquery_info i, processes p, time where p.pid = i.pid;
|
||||
*/
|
||||
const suggestions =
|
||||
isArray(ast?.columns) &&
|
||||
ast?.columns
|
||||
?.map((column) => {
|
||||
if (column.expr.column === '*') {
|
||||
return astOsqueryTables[column.expr.table].map((osqueryColumn) => ({
|
||||
label: osqueryColumn.name,
|
||||
value: {
|
||||
name: osqueryColumn.name,
|
||||
description: osqueryColumn.description,
|
||||
table: column.expr.table,
|
||||
suggestion_label: `${osqueryColumn.name}`,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
if (astOsqueryTables && astOsqueryTables[column.expr.table]) {
|
||||
const osqueryColumn = find(astOsqueryTables[column.expr.table], [
|
||||
'name',
|
||||
column.expr.column,
|
||||
]);
|
||||
|
||||
if (osqueryColumn) {
|
||||
const label = column.as ?? column.expr.column;
|
||||
|
||||
return [
|
||||
{
|
||||
label: column.as ?? column.expr.column,
|
||||
value: {
|
||||
name: osqueryColumn.name,
|
||||
description: osqueryColumn.description,
|
||||
table: column.expr.table,
|
||||
suggestion_label: `${label}`,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
})
|
||||
.flat();
|
||||
|
||||
// @ts-expect-error update types
|
||||
return sortBy(suggestions, 'value.suggestion_label');
|
||||
});
|
||||
}, [query]);
|
||||
|
||||
const handleAddRow = useCallback(
|
||||
(newRow) => {
|
||||
if (newRow?.key && newRow?.value) {
|
||||
setValue(
|
||||
produce((draft) => {
|
||||
draft[newRow.key] = newRow.value;
|
||||
return draft;
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const handleUpdateRow = useCallback(
|
||||
(currentKey: string) => (updatedRow: FormData) => {
|
||||
if (updatedRow?.key && updatedRow?.value) {
|
||||
setValue(
|
||||
produce((draft) => {
|
||||
if (currentKey !== updatedRow.key) {
|
||||
delete draft[currentKey];
|
||||
}
|
||||
|
||||
draft[updatedRow.key] = updatedRow.value;
|
||||
|
||||
return draft;
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const handleDeleteRow = useCallback(
|
||||
(key) => {
|
||||
if (key) {
|
||||
setValue(
|
||||
produce((draft) => {
|
||||
if (draft[key]) {
|
||||
delete draft[key];
|
||||
}
|
||||
return draft;
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryGroup.form.ecsMappingSection.title"
|
||||
defaultMessage="ECS mapping"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.scheduledQueryGroup.form.ecsMappingSection.description"
|
||||
defaultMessage="Use the fields below to map results from this query to ECS fields."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
{Object.entries(value).map(([ecsKey, ecsValue]) => (
|
||||
<ECSMappingEditorForm
|
||||
// eslint-disable-next-line
|
||||
ref={(formRef) => {
|
||||
if (formRef) {
|
||||
formRefs.current[ecsKey] = formRef;
|
||||
}
|
||||
}}
|
||||
key={ecsKey}
|
||||
osquerySchemaOptions={osquerySchemaOptions}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
defaultValue={{
|
||||
key: ecsKey,
|
||||
value: ecsValue,
|
||||
}}
|
||||
onChange={handleUpdateRow(ecsKey)}
|
||||
onDelete={handleDeleteRow}
|
||||
/>
|
||||
))}
|
||||
<ECSMappingEditorForm
|
||||
// eslint-disable-next-line
|
||||
ref={(formRef) => {
|
||||
if (formRef) {
|
||||
formRefs.current.new = formRef;
|
||||
}
|
||||
}}
|
||||
osquerySchemaOptions={osquerySchemaOptions}
|
||||
onAdd={handleAddRow}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ECSMappingEditorField;
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import type {
|
||||
ECSMappingEditorFieldProps,
|
||||
ECSMappingEditorFieldRef,
|
||||
} from './ecs_mapping_editor_field';
|
||||
|
||||
const LazyECSMappingEditorField = lazy(() => import('./ecs_mapping_editor_field'));
|
||||
|
||||
export type { ECSMappingEditorFieldProps, ECSMappingEditorFieldRef };
|
||||
export const ECSMappingEditorField = (props: ECSMappingEditorFieldProps) => (
|
||||
<Suspense fallback={null}>
|
||||
<LazyECSMappingEditorField {...props} />
|
||||
</Suspense>
|
||||
);
|
|
@ -20,22 +20,23 @@ import {
|
|||
EuiButton,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { satisfies } from 'semver';
|
||||
|
||||
import { OsqueryManagerPackagePolicyConfigRecord } from '../../../common/types';
|
||||
import { CodeEditorField } from '../../saved_queries/form/code_editor_field';
|
||||
import { Form, getUseField, Field } from '../../shared_imports';
|
||||
import { Form, getUseField, Field, useFormData } from '../../shared_imports';
|
||||
import { PlatformCheckBoxGroupField } from './platform_checkbox_group_field';
|
||||
import { ALL_OSQUERY_VERSIONS_OPTIONS } from './constants';
|
||||
import {
|
||||
UseScheduledQueryGroupQueryFormProps,
|
||||
ScheduledQueryGroupFormData,
|
||||
useScheduledQueryGroupQueryForm,
|
||||
} from './use_scheduled_query_group_query_form';
|
||||
import { ManageIntegrationLink } from '../../components/manage_integration_link';
|
||||
import { SavedQueriesDropdown } from '../../saved_queries/saved_queries_dropdown';
|
||||
import { ECSMappingEditorField, ECSMappingEditorFieldRef } from './lazy_ecs_mapping_editor_field';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
|
@ -43,7 +44,7 @@ interface QueryFlyoutProps {
|
|||
uniqueQueryIds: string[];
|
||||
defaultValue?: UseScheduledQueryGroupQueryFormProps['defaultValue'] | undefined;
|
||||
integrationPackageVersion?: string | undefined;
|
||||
onSave: (payload: OsqueryManagerPackagePolicyConfigRecord) => Promise<void>;
|
||||
onSave: (payload: ScheduledQueryGroupFormData) => Promise<void>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
@ -54,18 +55,25 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
onSave,
|
||||
onClose,
|
||||
}) => {
|
||||
const ecsFieldRef = useRef<ECSMappingEditorFieldRef>();
|
||||
const [isEditMode] = useState(!!defaultValue);
|
||||
const { form } = useScheduledQueryGroupQueryForm({
|
||||
uniqueQueryIds,
|
||||
defaultValue,
|
||||
handleSubmit: (payload, isValid) =>
|
||||
new Promise((resolve) => {
|
||||
if (isValid) {
|
||||
onSave(payload);
|
||||
handleSubmit: async (payload, isValid) => {
|
||||
const ecsFieldValue = await ecsFieldRef?.current?.validate();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (isValid && ecsFieldValue) {
|
||||
onSave({
|
||||
...payload,
|
||||
ecs_mapping: ecsFieldValue,
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
resolve();
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/* Platform and version fields are supported since osquery_manager@0.3.0 */
|
||||
|
@ -76,6 +84,11 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
|
||||
const { submit, setFieldValue, reset } = form;
|
||||
|
||||
const [{ query }] = useFormData({
|
||||
form,
|
||||
watch: ['query'],
|
||||
});
|
||||
|
||||
const handleSetQueryValue = useCallback(
|
||||
(savedQuery) => {
|
||||
if (!savedQuery) {
|
||||
|
@ -182,6 +195,16 @@ const QueryFlyoutComponent: React.FC<QueryFlyoutProps> = ({
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CommonUseField
|
||||
path="ecs_mapping"
|
||||
component={ECSMappingEditorField}
|
||||
query={query}
|
||||
fieldRef={ecsFieldRef}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Form>
|
||||
{!isFieldSupported ? (
|
||||
<EuiCallOut
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isArray, xor } from 'lodash';
|
||||
import { isArray, isEmpty, xor } from 'lodash';
|
||||
import uuid from 'uuid';
|
||||
import { produce } from 'immer';
|
||||
|
||||
|
@ -19,10 +19,7 @@ const FORM_ID = 'editQueryFlyoutForm';
|
|||
export interface UseScheduledQueryGroupQueryFormProps {
|
||||
uniqueQueryIds: string[];
|
||||
defaultValue?: OsqueryManagerPackagePolicyConfigRecord | undefined;
|
||||
handleSubmit: FormConfig<
|
||||
OsqueryManagerPackagePolicyConfigRecord,
|
||||
ScheduledQueryGroupFormData
|
||||
>['onSubmit'];
|
||||
handleSubmit: FormConfig<ScheduledQueryGroupFormData, ScheduledQueryGroupFormData>['onSubmit'];
|
||||
}
|
||||
|
||||
export interface ScheduledQueryGroupFormData {
|
||||
|
@ -31,6 +28,14 @@ export interface ScheduledQueryGroupFormData {
|
|||
interval: number;
|
||||
platform?: string | undefined;
|
||||
version?: string[] | undefined;
|
||||
ecs_mapping?:
|
||||
| Record<
|
||||
string,
|
||||
{
|
||||
field: string;
|
||||
}
|
||||
>
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export const useScheduledQueryGroupQueryForm = ({
|
||||
|
@ -51,6 +56,7 @@ export const useScheduledQueryGroupQueryForm = ({
|
|||
id: FORM_ID + uuid.v4(),
|
||||
onSubmit: async (formData, isValid) => {
|
||||
if (isValid && handleSubmit) {
|
||||
// @ts-expect-error update types
|
||||
return handleSubmit(formData, isValid);
|
||||
}
|
||||
},
|
||||
|
@ -76,6 +82,9 @@ export const useScheduledQueryGroupQueryForm = ({
|
|||
draft.version = draft.version[0];
|
||||
}
|
||||
}
|
||||
if (isEmpty(draft.ecs_mapping)) {
|
||||
delete draft.ecs_mapping;
|
||||
}
|
||||
return draft;
|
||||
}),
|
||||
deserializer: (payload) => {
|
||||
|
@ -87,6 +96,7 @@ export const useScheduledQueryGroupQueryForm = ({
|
|||
interval: parseInt(payload.interval.value, 10),
|
||||
platform: payload.platform?.value,
|
||||
version: payload.version?.value ? [payload.version?.value] : [],
|
||||
ecs_mapping: payload.ecs_mapping?.value ?? {},
|
||||
};
|
||||
},
|
||||
schema: formSchema,
|
||||
|
|
|
@ -25,6 +25,7 @@ export {
|
|||
useFormData,
|
||||
ValidationError,
|
||||
ValidationFunc,
|
||||
ValidationFuncArg,
|
||||
VALIDATION_TYPES,
|
||||
} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
|
||||
|
||||
|
|
|
@ -6,4 +6,4 @@
|
|||
*/
|
||||
|
||||
require('../../../../../src/setup_node_env');
|
||||
require('./script');
|
||||
require('./ecs_formatter');
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { map, partialRight, pick } from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { run } from '@kbn/dev-utils';
|
||||
|
||||
const ECS_COLUMN_SCHEMA_FIELDS = ['field', 'type', 'description'];
|
||||
|
||||
run(
|
||||
async ({ flags }) => {
|
||||
const schemaPath = path.resolve(`../public/common/schemas/ecs/`);
|
||||
const schemaFile = path.join(schemaPath, flags.schema_version as string);
|
||||
const schemaData = await require(schemaFile);
|
||||
|
||||
const formattedSchema = map(schemaData, partialRight(pick, ECS_COLUMN_SCHEMA_FIELDS));
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(schemaPath, `v${flags.schema_version}-formatted.json`),
|
||||
JSON.stringify(formattedSchema)
|
||||
);
|
||||
},
|
||||
{
|
||||
description: `
|
||||
Script for formatting generated osquery API schema JSON file.
|
||||
`,
|
||||
flags: {
|
||||
string: ['schema_version'],
|
||||
help: `
|
||||
--schema_version The semver string for the schema file located in public/common/schemas/ecs/
|
||||
`,
|
||||
},
|
||||
}
|
||||
);
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
require('../../../../../src/setup_node_env');
|
||||
require('./osquery_formatter');
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { map, partialRight, pick } from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { run } from '@kbn/dev-utils';
|
||||
|
||||
const OSQUERY_COLUMN_SCHEMA_FIELDS = ['name', 'description', 'platforms', 'columns'];
|
||||
|
||||
run(
|
||||
async ({ flags }) => {
|
||||
const schemaPath = path.resolve(`../public/common/schemas/osquery/`);
|
||||
const schemaFile = path.join(schemaPath, flags.schema_version as string);
|
||||
const schemaData = await require(schemaFile);
|
||||
|
||||
const formattedSchema = map(schemaData, partialRight(pick, OSQUERY_COLUMN_SCHEMA_FIELDS));
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(schemaPath, `v${flags.schema_version}-formatted.json`),
|
||||
JSON.stringify(formattedSchema)
|
||||
);
|
||||
},
|
||||
{
|
||||
description: `
|
||||
Script for formatting generated osquery API schema JSON file.
|
||||
`,
|
||||
flags: {
|
||||
string: ['schema_version'],
|
||||
help: `
|
||||
--schema_version The semver string for the schema file located in public/common/schemas/osquery/
|
||||
`,
|
||||
},
|
||||
}
|
||||
);
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { run } from '@kbn/dev-utils';
|
||||
interface DestField {
|
||||
[key: string]: boolean | DestField;
|
||||
}
|
||||
|
||||
run(
|
||||
async ({ flags }) => {
|
||||
const schemaPath = path.resolve('./public/editor/osquery_schema/');
|
||||
const schemaFile = path.join(schemaPath, flags.schema_version as string);
|
||||
const schemaData = await require(schemaFile);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function pullFields(destSchema: DestField, source: { [key: string]: any }) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const dest: { [key: string]: any } = {};
|
||||
Object.keys(destSchema).forEach((key) => {
|
||||
switch (typeof source[key]) {
|
||||
case 'object':
|
||||
dest[key] = pullFields(destSchema[key] as DestField, source[key]);
|
||||
break;
|
||||
default:
|
||||
dest[key] = source[key];
|
||||
}
|
||||
});
|
||||
return dest;
|
||||
}
|
||||
|
||||
const mapFunc = pullFields.bind(null, { name: true });
|
||||
const formattedSchema = schemaData.map(mapFunc);
|
||||
await fs.writeFile(
|
||||
path.join(schemaPath, `${flags.schema_version}-formatted.json`),
|
||||
JSON.stringify(formattedSchema)
|
||||
);
|
||||
},
|
||||
{
|
||||
description: `
|
||||
Script for formatting generated osquery API schema JSON file.
|
||||
`,
|
||||
flags: {
|
||||
string: ['schema_version'],
|
||||
help: `
|
||||
--schema_version The semver string for the schema file located in public/editor/osquery_schema
|
||||
`,
|
||||
},
|
||||
}
|
||||
);
|
|
@ -12,7 +12,9 @@
|
|||
"public/**/*",
|
||||
"scripts/**/*",
|
||||
"server/**/*",
|
||||
"../../../typings/**/*"
|
||||
"../../../typings/**/*",
|
||||
// ECS and Osquery schema files
|
||||
"public/common/schemas/*/**.json",
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../src/core/tsconfig.json" },
|
||||
|
|
|
@ -8359,7 +8359,7 @@ better-opn@^2.0.0:
|
|||
dependencies:
|
||||
open "^7.0.3"
|
||||
|
||||
big-integer@^1.6.16:
|
||||
big-integer@^1.6.16, big-integer@^1.6.48:
|
||||
version "1.6.48"
|
||||
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e"
|
||||
integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==
|
||||
|
@ -20390,6 +20390,13 @@ node-sass@^4.14.1:
|
|||
stdout-stream "^1.4.0"
|
||||
"true-case-path" "^1.0.2"
|
||||
|
||||
node-sql-parser@^3.6.1:
|
||||
version "3.6.1"
|
||||
resolved "https://registry.yarnpkg.com/node-sql-parser/-/node-sql-parser-3.6.1.tgz#6f096e9df1f19d1e2daa658d864bd68b0e2cd2c6"
|
||||
integrity sha512-AseDvELmUvL22L6C63DsTuzF+0i/HBIHjJq/uxC7jV3PGpAUib5Oe6oz4sgAniSUMPSZQbZmRore6Na68Sg4Tg==
|
||||
dependencies:
|
||||
big-integer "^1.6.48"
|
||||
|
||||
node-status-codes@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue