mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* Expose EQL search API configs in EQL rules #130339 * Update x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx Co-authored-by: Marshall Main <55718608+marshallmain@users.noreply.github.com> * Review comments * Wire the new fields through so they show up when viewing the rule details page * Integration tests * Update x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts Co-authored-by: Marshall Main <55718608+marshallmain@users.noreply.github.com> * Update x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx Co-authored-by: Marshall Main <55718608+marshallmain@users.noreply.github.com> * Update x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx Co-authored-by: Marshall Main <55718608+marshallmain@users.noreply.github.com> * Update x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx Co-authored-by: Marshall Main <55718608+marshallmain@users.noreply.github.com> * Fix unit tests * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * 400 error when trying to remove existing option while updating the rule Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Marshall Main <55718608+marshallmain@users.noreply.github.com> Co-authored-by: Garrett Spong <spong@users.noreply.github.com>
This commit is contained in:
parent
16e12a20bb
commit
1a90718596
48 changed files with 536 additions and 33 deletions
|
@ -65,6 +65,18 @@ export type EventCategoryOverride = t.TypeOf<typeof event_category_override>;
|
|||
export const eventCategoryOverrideOrUndefined = t.union([event_category_override, t.undefined]);
|
||||
export type EventCategoryOverrideOrUndefined = t.TypeOf<typeof eventCategoryOverrideOrUndefined>;
|
||||
|
||||
export const tiebreaker_field = t.string;
|
||||
export type TiebreakerField = t.TypeOf<typeof tiebreaker_field>;
|
||||
|
||||
export const tiebreakerFieldOrUndefined = t.union([tiebreaker_field, t.undefined]);
|
||||
export type TiebreakerFieldOrUndefined = t.TypeOf<typeof tiebreakerFieldOrUndefined>;
|
||||
|
||||
export const timestamp_field = t.string;
|
||||
export type TimestampField = t.TypeOf<typeof timestamp_field>;
|
||||
|
||||
export const timestampFieldOrUndefined = t.union([timestamp_field, t.undefined]);
|
||||
export type TimestampFieldOrUndefined = t.TypeOf<typeof timestampFieldOrUndefined>;
|
||||
|
||||
export const false_positives = t.array(t.string);
|
||||
export type FalsePositives = t.TypeOf<typeof false_positives>;
|
||||
|
||||
|
|
|
@ -70,7 +70,9 @@ import {
|
|||
rule_name_override,
|
||||
timestamp_override,
|
||||
Author,
|
||||
timestamp_field,
|
||||
event_category_override,
|
||||
tiebreaker_field,
|
||||
namespace,
|
||||
RelatedIntegrationArray,
|
||||
RequiredFieldArray,
|
||||
|
@ -105,7 +107,9 @@ export const addPrepackagedRulesSchema = t.intersection([
|
|||
author: DefaultStringArray, // defaults to empty array of strings if not set during decode
|
||||
building_block_type, // defaults to undefined if not set during decode
|
||||
enabled: DefaultBooleanFalse, // defaults to false if not set during decode
|
||||
timestamp_field, // defaults to "undefined" if not set during decode
|
||||
event_category_override, // defaults to "undefined" if not set during decode
|
||||
tiebreaker_field, // defaults to "undefined" if not set during decode
|
||||
false_positives: DefaultStringArray, // defaults to empty string array if not set during decode
|
||||
filters, // defaults to undefined if not set during decode
|
||||
from: DefaultFromString, // defaults to "now-6m" if not set during decode
|
||||
|
|
|
@ -79,7 +79,9 @@ import {
|
|||
rule_name_override,
|
||||
timestamp_override,
|
||||
Author,
|
||||
timestamp_field,
|
||||
event_category_override,
|
||||
tiebreaker_field,
|
||||
RelatedIntegrationArray,
|
||||
RequiredFieldArray,
|
||||
SetupGuide,
|
||||
|
@ -114,7 +116,9 @@ export const importRulesSchema = t.intersection([
|
|||
author: DefaultStringArray, // defaults to empty array of strings if not set during decode
|
||||
building_block_type, // defaults to undefined if not set during decode
|
||||
enabled: DefaultBooleanTrue, // defaults to true if not set during decode
|
||||
timestamp_field, // defaults to "undefined" if not set during decode
|
||||
event_category_override, // defaults to "undefined" if not set during decode
|
||||
tiebreaker_field, // defaults to "undefined" if not set during decode
|
||||
false_positives: DefaultStringArray, // defaults to empty string array if not set during decode
|
||||
filters, // defaults to undefined if not set during decode
|
||||
from: DefaultFromString, // defaults to "now-6m" if not set during decode
|
||||
|
|
|
@ -60,7 +60,9 @@ import {
|
|||
license,
|
||||
rule_name_override,
|
||||
timestamp_override,
|
||||
timestamp_field,
|
||||
event_category_override,
|
||||
tiebreaker_field,
|
||||
} from '../common';
|
||||
|
||||
/**
|
||||
|
@ -79,7 +81,9 @@ export const patchRulesSchema = t.exact(
|
|||
actions,
|
||||
anomaly_threshold,
|
||||
enabled,
|
||||
timestamp_field,
|
||||
event_category_override,
|
||||
tiebreaker_field,
|
||||
false_positives,
|
||||
filters,
|
||||
from,
|
||||
|
|
|
@ -33,7 +33,9 @@ import {
|
|||
id,
|
||||
index,
|
||||
filters,
|
||||
timestamp_field,
|
||||
event_category_override,
|
||||
tiebreaker_field,
|
||||
building_block_type,
|
||||
note,
|
||||
license,
|
||||
|
@ -213,7 +215,9 @@ const eqlRuleParams = {
|
|||
optional: {
|
||||
index,
|
||||
filters,
|
||||
timestamp_field,
|
||||
event_category_override,
|
||||
tiebreaker_field,
|
||||
},
|
||||
defaultable: {},
|
||||
};
|
||||
|
|
|
@ -790,7 +790,7 @@ describe('rules_schema', () => {
|
|||
|
||||
test('should return 3 fields for a rule of type "eql"', () => {
|
||||
const fields = addEqlFields({ type: 'eql' });
|
||||
expect(fields.length).toEqual(3);
|
||||
expect(fields.length).toEqual(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -41,7 +41,9 @@ import {
|
|||
anomaly_threshold,
|
||||
description,
|
||||
enabled,
|
||||
timestamp_field,
|
||||
event_category_override,
|
||||
tiebreaker_field,
|
||||
false_positives,
|
||||
id,
|
||||
immutable,
|
||||
|
@ -131,7 +133,9 @@ export const dependentRulesSchema = t.partial({
|
|||
query,
|
||||
|
||||
// eql only fields
|
||||
timestamp_field,
|
||||
event_category_override,
|
||||
tiebreaker_field,
|
||||
|
||||
// when type = saved_query, saved_id is required
|
||||
saved_id,
|
||||
|
@ -272,9 +276,11 @@ export const addThresholdFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.
|
|||
export const addEqlFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => {
|
||||
if (typeAndTimelineOnly.type === 'eql') {
|
||||
return [
|
||||
t.exact(t.partial({ timestamp_field: dependentRulesSchema.props.timestamp_field })),
|
||||
t.exact(
|
||||
t.partial({ event_category_override: dependentRulesSchema.props.event_category_override })
|
||||
),
|
||||
t.exact(t.partial({ tiebreaker_field: dependentRulesSchema.props.tiebreaker_field })),
|
||||
t.exact(t.type({ query: dependentRulesSchema.props.query })),
|
||||
t.exact(t.type({ language: dependentRulesSchema.props.language })),
|
||||
];
|
||||
|
|
|
@ -29,6 +29,7 @@ import { ThreatMapping, Type } from '@kbn/securitysolution-io-ts-alerting-types'
|
|||
import { getDisplayValueFromFilter } from '@kbn/data-plugin/public';
|
||||
import { FilterLabel } from '@kbn/unified-search-plugin/public';
|
||||
import { MATCHES, AND, OR } from '../../../../common/components/threat_match/translations';
|
||||
import { EqlOptionsSelected } from '../../../../../common/search_strategy';
|
||||
import { assertUnreachable } from '../../../../../common/utility_types';
|
||||
import * as i18nSeverity from '../severity_mapping/translations';
|
||||
import * as i18nRiskScore from '../risk_score_mapping/translations';
|
||||
|
@ -125,6 +126,38 @@ export const buildQueryBarDescription = ({
|
|||
return items;
|
||||
};
|
||||
|
||||
export const buildEqlOptionsDescription = (eqlOptions: EqlOptionsSelected): ListItems[] => {
|
||||
let items: ListItems[] = [];
|
||||
if (!isEmpty(eqlOptions.eventCategoryField)) {
|
||||
items = [
|
||||
...items,
|
||||
{
|
||||
title: <>{i18n.EQL_EVENT_CATEGORY_FIELD_LABEL}</>,
|
||||
description: <>{eqlOptions.eventCategoryField}</>,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (!isEmpty(eqlOptions.tiebreakerField)) {
|
||||
items = [
|
||||
...items,
|
||||
{
|
||||
title: <>{i18n.EQL_TIEBREAKER_FIELD_LABEL}</>,
|
||||
description: <>{eqlOptions.tiebreakerField}</>,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (!isEmpty(eqlOptions.timestampField)) {
|
||||
items = [
|
||||
...items,
|
||||
{
|
||||
title: <>{i18n.EQL_TIMESTAMP_FIELD_LABEL}</>,
|
||||
description: <>{eqlOptions.timestampField}</>,
|
||||
},
|
||||
];
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
const ThreatEuiFlexGroup = styled(EuiFlexGroup)`
|
||||
.euiFlexItem {
|
||||
margin-bottom: 0px;
|
||||
|
|
|
@ -19,6 +19,7 @@ import type {
|
|||
RequiredFieldArray,
|
||||
} from '../../../../../common/detection_engine/schemas/common';
|
||||
import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations';
|
||||
import { EqlOptionsSelected } from '../../../../../common/search_strategy';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { AboutStepRiskScore, AboutStepSeverity } from '../../../pages/detection_engine/rules/types';
|
||||
import { FieldValueTimeline } from '../pick_timeline';
|
||||
|
@ -36,6 +37,7 @@ import {
|
|||
buildRuleTypeDescription,
|
||||
buildThresholdDescription,
|
||||
buildThreatMappingDescription,
|
||||
buildEqlOptionsDescription,
|
||||
buildRequiredFieldsDescription,
|
||||
} from './helpers';
|
||||
import { buildMlJobsDescription } from './ml_job_description';
|
||||
|
@ -177,6 +179,9 @@ export const getDescriptionItem = (
|
|||
savedId,
|
||||
indexPatterns,
|
||||
});
|
||||
} else if (field === 'eqlOptions') {
|
||||
const eqlOptions: EqlOptionsSelected = get(field, data);
|
||||
return buildEqlOptionsDescription(eqlOptions);
|
||||
} else if (field === 'threat') {
|
||||
const threats: Threats = get(field, data);
|
||||
return buildThreatDescription({ label, threat: filterEmptyThreats(threats) });
|
||||
|
|
|
@ -98,6 +98,27 @@ export const THRESHOLD_RESULTS_AGGREGATED_BY = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const EQL_EVENT_CATEGORY_FIELD_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDescription.eqlEventCategoryFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Event category field',
|
||||
}
|
||||
);
|
||||
|
||||
export const EQL_TIEBREAKER_FIELD_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDescription.eqlTiebreakerFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Tiebreaker field',
|
||||
}
|
||||
);
|
||||
|
||||
export const EQL_TIMESTAMP_FIELD_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDescription.eqlTimestampFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Timestamp field',
|
||||
}
|
||||
);
|
||||
|
||||
export const RELATED_INTEGRATIONS_INSTALLED = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrationsInstalledDescription',
|
||||
{
|
||||
|
|
|
@ -35,7 +35,8 @@ export interface EqlQueryBarProps {
|
|||
idAria?: string;
|
||||
optionsData?: EqlOptionsData;
|
||||
optionsSelected?: EqlOptionsSelected;
|
||||
onOptionsChange?: (field: FieldsEqlOptions, newValue: string | null) => void;
|
||||
isSizeOptionDisabled?: boolean;
|
||||
onOptionsChange?: (field: FieldsEqlOptions, newValue: string | undefined) => void;
|
||||
onValidityChange?: (arg: boolean) => void;
|
||||
onValiditingChange?: (arg: boolean) => void;
|
||||
}
|
||||
|
@ -46,6 +47,7 @@ export const EqlQueryBar: FC<EqlQueryBarProps> = ({
|
|||
idAria,
|
||||
optionsData,
|
||||
optionsSelected,
|
||||
isSizeOptionDisabled,
|
||||
onOptionsChange,
|
||||
onValidityChange,
|
||||
onValiditingChange,
|
||||
|
@ -121,6 +123,7 @@ export const EqlQueryBar: FC<EqlQueryBarProps> = ({
|
|||
<EqlQueryBarFooter
|
||||
errors={errorMessages}
|
||||
isLoading={isValidating}
|
||||
isSizeOptionDisabled={isSizeOptionDisabled}
|
||||
optionsData={optionsData}
|
||||
optionsSelected={optionsSelected}
|
||||
onOptionsChange={onOptionsChange}
|
||||
|
|
|
@ -34,9 +34,10 @@ import { EqlOverviewLink } from './eql_overview_link';
|
|||
export interface Props {
|
||||
errors: string[];
|
||||
isLoading?: boolean;
|
||||
isSizeOptionDisabled?: boolean;
|
||||
optionsData?: EqlOptionsData;
|
||||
optionsSelected?: EqlOptionsSelected;
|
||||
onOptionsChange?: (field: FieldsEqlOptions, newValue: string | null) => void;
|
||||
onOptionsChange?: (field: FieldsEqlOptions, newValue: string | undefined) => void;
|
||||
}
|
||||
|
||||
type SizeVoidFunc = (newSize: string) => void;
|
||||
|
@ -68,6 +69,7 @@ const singleSelection = { asPlainText: true };
|
|||
export const EqlQueryBarFooter: FC<Props> = ({
|
||||
errors,
|
||||
isLoading,
|
||||
isSizeOptionDisabled,
|
||||
optionsData,
|
||||
optionsSelected,
|
||||
onOptionsChange,
|
||||
|
@ -89,7 +91,7 @@ export const EqlQueryBarFooter: FC<Props> = ({
|
|||
if (opt.length > 0) {
|
||||
onOptionsChange('eventCategoryField', opt[0].label);
|
||||
} else {
|
||||
onOptionsChange('eventCategoryField', null);
|
||||
onOptionsChange('eventCategoryField', undefined);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -101,7 +103,7 @@ export const EqlQueryBarFooter: FC<Props> = ({
|
|||
if (opt.length > 0) {
|
||||
onOptionsChange('tiebreakerField', opt[0].label);
|
||||
} else {
|
||||
onOptionsChange('tiebreakerField', null);
|
||||
onOptionsChange('tiebreakerField', undefined);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -113,7 +115,7 @@ export const EqlQueryBarFooter: FC<Props> = ({
|
|||
if (opt.length > 0) {
|
||||
onOptionsChange('timestampField', opt[0].label);
|
||||
} else {
|
||||
onOptionsChange('timestampField', null);
|
||||
onOptionsChange('timestampField', undefined);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -192,17 +194,19 @@ export const EqlQueryBarFooter: FC<Props> = ({
|
|||
>
|
||||
<EuiPopoverTitle>{i18n.EQL_SETTINGS_TITLE}</EuiPopoverTitle>
|
||||
<div style={{ width: '300px' }}>
|
||||
<EuiFormRow
|
||||
label={i18n.EQL_OPTIONS_SIZE_LABEL}
|
||||
helpText={i18n.EQL_OPTIONS_SIZE_HELPER}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
value={localSize}
|
||||
onChange={handleSizeField}
|
||||
min={1}
|
||||
max={10000}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{!isSizeOptionDisabled && (
|
||||
<EuiFormRow
|
||||
label={i18n.EQL_OPTIONS_SIZE_LABEL}
|
||||
helpText={i18n.EQL_OPTIONS_SIZE_HELPER}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
value={localSize}
|
||||
onChange={handleSizeField}
|
||||
min={1}
|
||||
max={10000}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
<EuiFormRow
|
||||
label={i18n.EQL_OPTIONS_EVENT_CATEGORY_FIELD_LABEL}
|
||||
helpText={i18n.EQL_OPTIONS_EVENT_CATEGORY_FIELD_HELPER}
|
||||
|
|
|
@ -55,6 +55,7 @@ const defaultProps: RulePreviewProps = {
|
|||
},
|
||||
anomalyThreshold: 50,
|
||||
machineLearningJobId: ['test-ml-job-id'],
|
||||
eqlOptions: {},
|
||||
};
|
||||
|
||||
describe('PreviewQuery', () => {
|
||||
|
|
|
@ -28,6 +28,7 @@ import { useKibana } from '../../../../common/lib/kibana';
|
|||
import { LoadingHistogram } from './loading_histogram';
|
||||
import { FieldValueThreshold } from '../threshold_input';
|
||||
import { isJobStarted } from '../../../../../common/machine_learning/helpers';
|
||||
import { EqlOptionsSelected } from '../../../../../common/search_strategy';
|
||||
|
||||
const HelpTextComponent = (
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
|
@ -47,6 +48,7 @@ export interface RulePreviewProps {
|
|||
threshold: FieldValueThreshold;
|
||||
machineLearningJobId: string[];
|
||||
anomalyThreshold: number;
|
||||
eqlOptions: EqlOptionsSelected;
|
||||
}
|
||||
|
||||
const Select = styled(EuiSelect)`
|
||||
|
@ -70,6 +72,7 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
eqlOptions,
|
||||
}) => {
|
||||
const { spaces } = useKibana().services;
|
||||
const { loading: isMlLoading, jobs } = useSecurityJobs(false);
|
||||
|
@ -112,6 +115,7 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
|
|||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
eqlOptions,
|
||||
});
|
||||
|
||||
// Resets the timeFrame to default when rule type is changed because not all time frames are supported by all rule types
|
||||
|
|
|
@ -13,6 +13,7 @@ import { usePreviewRule } from '../../../containers/detection_engine/rules/use_p
|
|||
import { formatPreviewRule } from '../../../pages/detection_engine/rules/create/helpers';
|
||||
import { FieldValueThreshold } from '../threshold_input';
|
||||
import { RulePreviewLogs } from '../../../../../common/detection_engine/schemas/request';
|
||||
import { EqlOptionsSelected } from '../../../../../common/search_strategy';
|
||||
|
||||
interface PreviewRouteParams {
|
||||
isDisabled: boolean;
|
||||
|
@ -26,6 +27,7 @@ interface PreviewRouteParams {
|
|||
threshold: FieldValueThreshold;
|
||||
machineLearningJobId: string[];
|
||||
anomalyThreshold: number;
|
||||
eqlOptions: EqlOptionsSelected;
|
||||
}
|
||||
|
||||
export const usePreviewRoute = ({
|
||||
|
@ -40,6 +42,7 @@ export const usePreviewRoute = ({
|
|||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
eqlOptions,
|
||||
}: PreviewRouteParams) => {
|
||||
const [isRequestTriggered, setIsRequestTriggered] = useState(false);
|
||||
|
||||
|
@ -80,6 +83,7 @@ export const usePreviewRoute = ({
|
|||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
eqlOptions,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -96,6 +100,7 @@ export const usePreviewRoute = ({
|
|||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
eqlOptions,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -113,6 +118,7 @@ export const usePreviewRoute = ({
|
|||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
eqlOptions,
|
||||
]);
|
||||
|
||||
return {
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
*/
|
||||
|
||||
import { EuiButtonEmpty, EuiFormRow, EuiSpacer } from '@elastic/eui';
|
||||
import React, { FC, memo, useCallback, useState, useEffect } from 'react';
|
||||
import React, { FC, memo, useCallback, useMemo, useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { isEqual } from 'lodash';
|
||||
import { isEqual, isEmpty } from 'lodash';
|
||||
import { FieldSpec } from '@kbn/data-views-plugin/common';
|
||||
|
||||
import {
|
||||
DEFAULT_INDEX_KEY,
|
||||
|
@ -21,6 +22,7 @@ import { hasMlAdminPermissions } from '../../../../../common/machine_learning/ha
|
|||
import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license';
|
||||
import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities';
|
||||
import { useUiSetting$ } from '../../../../common/lib/kibana';
|
||||
import { EqlOptionsSelected, FieldsEqlOptions } from '../../../../../common/search_strategy';
|
||||
import { filterRuleFieldsForType } from '../../../pages/detection_engine/rules/create/helpers';
|
||||
import {
|
||||
DefineStepRule,
|
||||
|
@ -95,6 +97,7 @@ export const stepDefineDefaultValue: DefineStepRule = {
|
|||
id: null,
|
||||
title: DEFAULT_TIMELINE_TITLE,
|
||||
},
|
||||
eqlOptions: {},
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -194,6 +197,9 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
const ruleType = formRuleType || initialState.ruleType;
|
||||
const [indexPatternsLoading, { browserFields, indexPatterns }] = useFetchIndex(index);
|
||||
const fields: Readonly<BrowserFields> = aggregatableFields(browserFields);
|
||||
const [optionsSelected, setOptionsSelected] = useState<EqlOptionsSelected>(
|
||||
defaultValues?.eqlOptions || {}
|
||||
);
|
||||
|
||||
const [
|
||||
threatIndexPatternsLoading,
|
||||
|
@ -253,13 +259,20 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
|
||||
const getData = useCallback(async () => {
|
||||
const result = await submit();
|
||||
result.data = {
|
||||
...result.data,
|
||||
eqlOptions: optionsSelected,
|
||||
};
|
||||
return result.isValid
|
||||
? result
|
||||
: {
|
||||
isValid: false,
|
||||
data: getFormData(),
|
||||
data: {
|
||||
...getFormData(),
|
||||
eqlOptions: optionsSelected,
|
||||
},
|
||||
};
|
||||
}, [getFormData, submit]);
|
||||
}, [getFormData, optionsSelected, submit]);
|
||||
|
||||
useEffect(() => {
|
||||
let didCancel = false;
|
||||
|
@ -324,6 +337,36 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
threatIndexPatternsLoading,
|
||||
]
|
||||
);
|
||||
|
||||
const onOptionsChange = useCallback((field: FieldsEqlOptions, value: string | undefined) => {
|
||||
setOptionsSelected((prevOptions) => ({
|
||||
...prevOptions,
|
||||
[field]: value,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const optionsData = useMemo(
|
||||
() =>
|
||||
isEmpty(indexPatterns.fields)
|
||||
? {
|
||||
keywordFields: [],
|
||||
dateFields: [],
|
||||
nonDateFields: [],
|
||||
}
|
||||
: {
|
||||
keywordFields: (indexPatterns.fields as FieldSpec[])
|
||||
.filter((f) => f.esTypes?.includes('keyword'))
|
||||
.map((f) => ({ label: f.name })),
|
||||
dateFields: indexPatterns.fields
|
||||
.filter((f) => f.type === 'date')
|
||||
.map((f) => ({ label: f.name })),
|
||||
nonDateFields: indexPatterns.fields
|
||||
.filter((f) => f.type !== 'date')
|
||||
.map((f) => ({ label: f.name })),
|
||||
},
|
||||
[indexPatterns]
|
||||
);
|
||||
|
||||
return isReadOnlyView ? (
|
||||
<StepContentWrapper data-test-subj="definitionRule" addPadding={addPadding}>
|
||||
<StepRuleDescription
|
||||
|
@ -374,6 +417,10 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
path="queryBar"
|
||||
component={EqlQueryBar}
|
||||
componentProps={{
|
||||
optionsData,
|
||||
optionsSelected,
|
||||
isSizeOptionDisabled: true,
|
||||
onOptionsChange,
|
||||
onValidityChange: setIsQueryBarValid,
|
||||
idAria: 'detectionEngineStepDefineRuleEqlQueryBar',
|
||||
isDisabled: isLoading,
|
||||
|
@ -509,6 +556,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
threshold={formThreshold}
|
||||
machineLearningJobId={machineLearningJobId}
|
||||
anomalyThreshold={anomalyThreshold}
|
||||
eqlOptions={optionsSelected}
|
||||
/>
|
||||
</StepContentWrapper>
|
||||
|
||||
|
|
|
@ -77,6 +77,7 @@ export const schema: FormSchema<DefineStepRule> = {
|
|||
},
|
||||
],
|
||||
},
|
||||
eqlOptions: {},
|
||||
queryBar: {
|
||||
validations: [
|
||||
{
|
||||
|
|
|
@ -30,6 +30,9 @@ import {
|
|||
license,
|
||||
rule_name_override,
|
||||
timestamp_override,
|
||||
timestamp_field,
|
||||
event_category_override,
|
||||
tiebreaker_field,
|
||||
threshold,
|
||||
BulkAction,
|
||||
BulkActionEditPayload,
|
||||
|
@ -148,6 +151,9 @@ export const RuleSchema = t.intersection([
|
|||
timeline_id: t.string,
|
||||
timeline_title: t.string,
|
||||
timestamp_override,
|
||||
timestamp_field,
|
||||
event_category_override,
|
||||
tiebreaker_field,
|
||||
note: t.string,
|
||||
exceptions_list: listArray,
|
||||
uuid: t.string,
|
||||
|
|
|
@ -211,6 +211,7 @@ export const mockDefineStepRule = (): DefineStepRule => ({
|
|||
value: '2',
|
||||
},
|
||||
},
|
||||
eqlOptions: {},
|
||||
});
|
||||
|
||||
export const mockScheduleStepRule = (): ScheduleStepRule => ({
|
||||
|
|
|
@ -290,6 +290,40 @@ describe('helpers', () => {
|
|||
expect(result).toEqual(expect.objectContaining(expected));
|
||||
});
|
||||
|
||||
test('returns option fields if specified for eql type', () => {
|
||||
const mockStepData: DefineStepRule = {
|
||||
...mockData,
|
||||
ruleType: 'eql',
|
||||
queryBar: {
|
||||
...mockData.queryBar,
|
||||
query: {
|
||||
...mockData.queryBar.query,
|
||||
language: 'eql',
|
||||
query: 'process where process_name == "explorer.exe"',
|
||||
},
|
||||
},
|
||||
eqlOptions: {
|
||||
timestampField: 'event.created',
|
||||
tiebreakerField: 'process.name',
|
||||
eventCategoryField: 'event.action',
|
||||
},
|
||||
};
|
||||
const result = formatDefineStepData(mockStepData);
|
||||
|
||||
const expected: DefineStepRuleJson = {
|
||||
filters: mockStepData.queryBar.filters,
|
||||
index: mockStepData.index,
|
||||
language: 'eql',
|
||||
query: 'process where process_name == "explorer.exe"',
|
||||
type: 'eql',
|
||||
timestamp_field: 'event.created',
|
||||
tiebreaker_field: 'process.name',
|
||||
event_category_override: 'event.action',
|
||||
};
|
||||
|
||||
expect(result).toEqual(expect.objectContaining(expected));
|
||||
});
|
||||
|
||||
test('returns expected indicator matching rule type if all fields are filled out', () => {
|
||||
const threatFilters: DefineStepRule['threatQueryBar']['filters'] = [
|
||||
{
|
||||
|
|
|
@ -45,6 +45,7 @@ import { stepDefineDefaultValue } from '../../../../components/rules/step_define
|
|||
import { stepAboutDefaultValue } from '../../../../components/rules/step_about_rule/default_value';
|
||||
import { stepActionsDefaultValue } from '../../../../components/rules/step_rule_actions';
|
||||
import { FieldValueThreshold } from '../../../../components/rules/threshold_input';
|
||||
import { EqlOptionsSelected } from '../../../../../../common/search_strategy';
|
||||
|
||||
export const getTimeTypeValue = (time: string): { unit: string; value: number } => {
|
||||
const timeObj = {
|
||||
|
@ -94,6 +95,7 @@ export interface RuleFields {
|
|||
threatQueryBar?: unknown;
|
||||
threatMapping?: unknown;
|
||||
threatLanguage?: unknown;
|
||||
eqlOptions: unknown;
|
||||
}
|
||||
|
||||
type QueryRuleFields<T> = Omit<
|
||||
|
@ -104,33 +106,86 @@ type QueryRuleFields<T> = Omit<
|
|||
| 'threatIndex'
|
||||
| 'threatQueryBar'
|
||||
| 'threatMapping'
|
||||
| 'eqlOptions'
|
||||
>;
|
||||
type EqlQueryRuleFields<T> = Omit<
|
||||
T,
|
||||
| 'anomalyThreshold'
|
||||
| 'machineLearningJobId'
|
||||
| 'threshold'
|
||||
| 'threatIndex'
|
||||
| 'threatQueryBar'
|
||||
| 'threatMapping'
|
||||
>;
|
||||
type ThresholdRuleFields<T> = Omit<
|
||||
T,
|
||||
'anomalyThreshold' | 'machineLearningJobId' | 'threatIndex' | 'threatQueryBar' | 'threatMapping'
|
||||
| 'anomalyThreshold'
|
||||
| 'machineLearningJobId'
|
||||
| 'threatIndex'
|
||||
| 'threatQueryBar'
|
||||
| 'threatMapping'
|
||||
| 'eqlOptions'
|
||||
>;
|
||||
type MlRuleFields<T> = Omit<
|
||||
T,
|
||||
'queryBar' | 'index' | 'threshold' | 'threatIndex' | 'threatQueryBar' | 'threatMapping'
|
||||
| 'queryBar'
|
||||
| 'index'
|
||||
| 'threshold'
|
||||
| 'threatIndex'
|
||||
| 'threatQueryBar'
|
||||
| 'threatMapping'
|
||||
| 'eqlOptions'
|
||||
>;
|
||||
type ThreatMatchRuleFields<T> = Omit<
|
||||
T,
|
||||
'anomalyThreshold' | 'machineLearningJobId' | 'threshold' | 'eqlOptions'
|
||||
>;
|
||||
type ThreatMatchRuleFields<T> = Omit<T, 'anomalyThreshold' | 'machineLearningJobId' | 'threshold'>;
|
||||
|
||||
const isMlFields = <T>(
|
||||
fields: QueryRuleFields<T> | MlRuleFields<T> | ThresholdRuleFields<T> | ThreatMatchRuleFields<T>
|
||||
fields:
|
||||
| QueryRuleFields<T>
|
||||
| EqlQueryRuleFields<T>
|
||||
| MlRuleFields<T>
|
||||
| ThresholdRuleFields<T>
|
||||
| ThreatMatchRuleFields<T>
|
||||
): fields is MlRuleFields<T> => has('anomalyThreshold', fields);
|
||||
|
||||
const isThresholdFields = <T>(
|
||||
fields: QueryRuleFields<T> | MlRuleFields<T> | ThresholdRuleFields<T> | ThreatMatchRuleFields<T>
|
||||
fields:
|
||||
| QueryRuleFields<T>
|
||||
| EqlQueryRuleFields<T>
|
||||
| MlRuleFields<T>
|
||||
| ThresholdRuleFields<T>
|
||||
| ThreatMatchRuleFields<T>
|
||||
): fields is ThresholdRuleFields<T> => has('threshold', fields);
|
||||
|
||||
const isThreatMatchFields = <T>(
|
||||
fields: QueryRuleFields<T> | MlRuleFields<T> | ThresholdRuleFields<T> | ThreatMatchRuleFields<T>
|
||||
fields:
|
||||
| QueryRuleFields<T>
|
||||
| EqlQueryRuleFields<T>
|
||||
| MlRuleFields<T>
|
||||
| ThresholdRuleFields<T>
|
||||
| ThreatMatchRuleFields<T>
|
||||
): fields is ThreatMatchRuleFields<T> => has('threatIndex', fields);
|
||||
|
||||
const isEqlFields = <T>(
|
||||
fields:
|
||||
| QueryRuleFields<T>
|
||||
| EqlQueryRuleFields<T>
|
||||
| MlRuleFields<T>
|
||||
| ThresholdRuleFields<T>
|
||||
| ThreatMatchRuleFields<T>
|
||||
): fields is EqlQueryRuleFields<T> => has('eqlOptions', fields);
|
||||
|
||||
export const filterRuleFieldsForType = <T extends Partial<RuleFields>>(
|
||||
fields: T,
|
||||
type: Type
|
||||
): QueryRuleFields<T> | MlRuleFields<T> | ThresholdRuleFields<T> | ThreatMatchRuleFields<T> => {
|
||||
):
|
||||
| QueryRuleFields<T>
|
||||
| EqlQueryRuleFields<T>
|
||||
| MlRuleFields<T>
|
||||
| ThresholdRuleFields<T>
|
||||
| ThreatMatchRuleFields<T> => {
|
||||
switch (type) {
|
||||
case 'machine_learning':
|
||||
const {
|
||||
|
@ -140,6 +195,7 @@ export const filterRuleFieldsForType = <T extends Partial<RuleFields>>(
|
|||
threatIndex,
|
||||
threatQueryBar,
|
||||
threatMapping,
|
||||
eqlOptions,
|
||||
...mlRuleFields
|
||||
} = fields;
|
||||
return mlRuleFields;
|
||||
|
@ -150,6 +206,7 @@ export const filterRuleFieldsForType = <T extends Partial<RuleFields>>(
|
|||
threatIndex: _removedThreatIndex,
|
||||
threatQueryBar: _removedThreatQueryBar,
|
||||
threatMapping: _removedThreatMapping,
|
||||
eqlOptions: _eqlOptions,
|
||||
...thresholdRuleFields
|
||||
} = fields;
|
||||
return thresholdRuleFields;
|
||||
|
@ -158,12 +215,12 @@ export const filterRuleFieldsForType = <T extends Partial<RuleFields>>(
|
|||
anomalyThreshold: _removedAnomalyThreshold,
|
||||
machineLearningJobId: _removedMachineLearningJobId,
|
||||
threshold: _removedThreshold,
|
||||
eqlOptions: __eqlOptions,
|
||||
...threatMatchRuleFields
|
||||
} = fields;
|
||||
return threatMatchRuleFields;
|
||||
case 'query':
|
||||
case 'saved_query':
|
||||
case 'eql':
|
||||
const {
|
||||
anomalyThreshold: _a,
|
||||
machineLearningJobId: _m,
|
||||
|
@ -171,9 +228,21 @@ export const filterRuleFieldsForType = <T extends Partial<RuleFields>>(
|
|||
threatIndex: __removedThreatIndex,
|
||||
threatQueryBar: __removedThreatQueryBar,
|
||||
threatMapping: __removedThreatMapping,
|
||||
eqlOptions: ___eqlOptions,
|
||||
...queryRuleFields
|
||||
} = fields;
|
||||
return queryRuleFields;
|
||||
case 'eql':
|
||||
const {
|
||||
anomalyThreshold: __a,
|
||||
machineLearningJobId: __m,
|
||||
threshold: __t,
|
||||
threatIndex: ___removedThreatIndex,
|
||||
threatQueryBar: ___removedThreatQueryBar,
|
||||
threatMapping: ___removedThreatMapping,
|
||||
...eqlRuleFields
|
||||
} = fields;
|
||||
return eqlRuleFields;
|
||||
}
|
||||
assertUnreachable(type);
|
||||
};
|
||||
|
@ -258,6 +327,17 @@ export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStep
|
|||
threat_mapping: ruleFields.threatMapping,
|
||||
threat_language: ruleFields.threatQueryBar?.query?.language,
|
||||
}
|
||||
: isEqlFields(ruleFields)
|
||||
? {
|
||||
index: ruleFields.index,
|
||||
filters: ruleFields.queryBar?.filters,
|
||||
language: ruleFields.queryBar?.query?.language,
|
||||
query: ruleFields.queryBar?.query?.query as string,
|
||||
saved_id: ruleFields.queryBar?.saved_id,
|
||||
timestamp_field: ruleFields.eqlOptions?.timestampField,
|
||||
event_category_override: ruleFields.eqlOptions?.eventCategoryField,
|
||||
tiebreaker_field: ruleFields.eqlOptions?.tiebreakerField,
|
||||
}
|
||||
: {
|
||||
index: ruleFields.index,
|
||||
filters: ruleFields.queryBar?.filters,
|
||||
|
@ -405,6 +485,7 @@ export const formatPreviewRule = ({
|
|||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
eqlOptions,
|
||||
}: {
|
||||
index: string[];
|
||||
threatIndex: string[];
|
||||
|
@ -416,6 +497,7 @@ export const formatPreviewRule = ({
|
|||
threshold: FieldValueThreshold;
|
||||
machineLearningJobId: string[];
|
||||
anomalyThreshold: number;
|
||||
eqlOptions: EqlOptionsSelected;
|
||||
}): CreateRulesSchema => {
|
||||
const defineStepData = {
|
||||
...stepDefineDefaultValue,
|
||||
|
@ -428,6 +510,7 @@ export const formatPreviewRule = ({
|
|||
threshold,
|
||||
machineLearningJobId,
|
||||
anomalyThreshold,
|
||||
eqlOptions,
|
||||
};
|
||||
const aboutStepData = {
|
||||
...stepAboutDefaultValue,
|
||||
|
|
|
@ -105,6 +105,11 @@ describe('rule helpers', () => {
|
|||
id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2',
|
||||
title: 'Titled timeline',
|
||||
},
|
||||
eqlOptions: {
|
||||
timestampField: undefined,
|
||||
eventCategoryField: undefined,
|
||||
tiebreakerField: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const aboutRuleStepData: AboutStepRule = {
|
||||
|
@ -237,6 +242,11 @@ describe('rule helpers', () => {
|
|||
id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2',
|
||||
title: 'Untitled timeline',
|
||||
},
|
||||
eqlOptions: {
|
||||
timestampField: undefined,
|
||||
eventCategoryField: undefined,
|
||||
tiebreakerField: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
@ -281,6 +291,11 @@ describe('rule helpers', () => {
|
|||
id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2',
|
||||
title: 'Untitled timeline',
|
||||
},
|
||||
eqlOptions: {
|
||||
timestampField: undefined,
|
||||
eventCategoryField: undefined,
|
||||
tiebreakerField: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
|
|
@ -113,6 +113,11 @@ export const getDefineStepsData = (rule: Rule): DefineStepRule => ({
|
|||
}
|
||||
: {}),
|
||||
},
|
||||
eqlOptions: {
|
||||
timestampField: rule.timestamp_field,
|
||||
eventCategoryField: rule.event_category_override,
|
||||
tiebreakerField: rule.tiebreaker_field,
|
||||
},
|
||||
});
|
||||
|
||||
export const getScheduleStepsData = (rule: Rule): ScheduleStepRule => {
|
||||
|
|
|
@ -32,6 +32,7 @@ import type {
|
|||
SetupGuide,
|
||||
TimestampOverride,
|
||||
} from '../../../../../common/detection_engine/schemas/common';
|
||||
import { EqlOptionsSelected } from '../../../../../common/search_strategy';
|
||||
|
||||
export interface EuiBasicTableSortTypes {
|
||||
field: string;
|
||||
|
@ -140,6 +141,7 @@ export interface DefineStepRule {
|
|||
threatIndex: ThreatIndex;
|
||||
threatQueryBar: FieldValueQueryBar;
|
||||
threatMapping: ThreatMapping;
|
||||
eqlOptions: EqlOptionsSelected;
|
||||
}
|
||||
|
||||
export interface ScheduleStepRule {
|
||||
|
@ -179,6 +181,9 @@ export interface DefineStepRuleJson {
|
|||
timeline_id?: string;
|
||||
timeline_title?: string;
|
||||
type: Type;
|
||||
timestamp_field?: string;
|
||||
event_category_override?: string;
|
||||
tiebreaker_field?: string;
|
||||
}
|
||||
|
||||
export interface AboutStepRuleJson {
|
||||
|
|
|
@ -90,7 +90,7 @@ export const EqlQueryBarTimeline = memo(({ timelineId }: { timelineId: string })
|
|||
const { getFields } = form;
|
||||
|
||||
const onOptionsChange = useCallback(
|
||||
(field: FieldsEqlOptions, value: string | null) =>
|
||||
(field: FieldsEqlOptions, value: string | undefined) =>
|
||||
dispatch(
|
||||
timelineActions.updateEqlOptions({
|
||||
id: timelineId,
|
||||
|
|
|
@ -225,5 +225,5 @@ export const toggleModalSaveTimeline = actionCreator<{
|
|||
export const updateEqlOptions = actionCreator<{
|
||||
id: string;
|
||||
field: FieldsEqlOptions;
|
||||
value: string | null;
|
||||
value: string | undefined;
|
||||
}>('UPDATE_EQL_OPTIONS_TIMELINE');
|
||||
|
|
|
@ -73,7 +73,9 @@ export const patchRulesBulkRoute = (
|
|||
building_block_type: buildingBlockType,
|
||||
description,
|
||||
enabled,
|
||||
timestamp_field: timestampField,
|
||||
event_category_override: eventCategoryOverride,
|
||||
tiebreaker_field: tiebreakerField,
|
||||
false_positives: falsePositives,
|
||||
from,
|
||||
query,
|
||||
|
@ -152,7 +154,9 @@ export const patchRulesBulkRoute = (
|
|||
buildingBlockType,
|
||||
description,
|
||||
enabled,
|
||||
timestampField,
|
||||
eventCategoryOverride,
|
||||
tiebreakerField,
|
||||
falsePositives,
|
||||
from,
|
||||
query,
|
||||
|
|
|
@ -52,7 +52,9 @@ export const patchRulesRoute = (router: SecuritySolutionPluginRouter, ml: SetupP
|
|||
building_block_type: buildingBlockType,
|
||||
description,
|
||||
enabled,
|
||||
timestamp_field: timestampField,
|
||||
event_category_override: eventCategoryOverride,
|
||||
tiebreaker_field: tiebreakerField,
|
||||
false_positives: falsePositives,
|
||||
from,
|
||||
query,
|
||||
|
@ -139,7 +141,9 @@ export const patchRulesRoute = (router: SecuritySolutionPluginRouter, ml: SetupP
|
|||
buildingBlockType,
|
||||
description,
|
||||
enabled,
|
||||
timestampField,
|
||||
eventCategoryOverride,
|
||||
tiebreakerField,
|
||||
falsePositives,
|
||||
from,
|
||||
query,
|
||||
|
|
|
@ -101,7 +101,9 @@ export const importRules = async ({
|
|||
building_block_type: buildingBlockType,
|
||||
description,
|
||||
enabled,
|
||||
timestamp_field: timestampField,
|
||||
event_category_override: eventCategoryOverride,
|
||||
tiebreaker_field: tiebreakerField,
|
||||
false_positives: falsePositives,
|
||||
from,
|
||||
immutable,
|
||||
|
@ -176,7 +178,9 @@ export const importRules = async ({
|
|||
buildingBlockType,
|
||||
description,
|
||||
enabled,
|
||||
timestampField,
|
||||
eventCategoryOverride,
|
||||
tiebreakerField,
|
||||
falsePositives,
|
||||
from,
|
||||
immutable,
|
||||
|
@ -240,7 +244,9 @@ export const importRules = async ({
|
|||
buildingBlockType,
|
||||
description,
|
||||
enabled,
|
||||
timestampField,
|
||||
eventCategoryOverride,
|
||||
tiebreakerField,
|
||||
falsePositives,
|
||||
from,
|
||||
query,
|
||||
|
|
|
@ -15,7 +15,9 @@ export const createRuleMock = (params: Partial<RuleParams>) => ({
|
|||
createdBy: 'elastic',
|
||||
description: '24/7',
|
||||
enabled: true,
|
||||
timestampField: undefined,
|
||||
eventCategoryOverride: undefined,
|
||||
tiebreakerField: undefined,
|
||||
exceptionsList: [],
|
||||
falsePositives: [],
|
||||
filters: [],
|
||||
|
|
|
@ -15,7 +15,9 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({
|
|||
anomalyThreshold: undefined,
|
||||
description: 'some description',
|
||||
enabled: true,
|
||||
timestampField: undefined,
|
||||
eventCategoryOverride: undefined,
|
||||
tiebreakerField: undefined,
|
||||
falsePositives: ['false positive 1', 'false positive 2'],
|
||||
from: 'now-6m',
|
||||
query: 'user.name: root or user.name: admin',
|
||||
|
@ -71,7 +73,9 @@ export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({
|
|||
anomalyThreshold: 55,
|
||||
description: 'some description',
|
||||
enabled: true,
|
||||
timestampField: undefined,
|
||||
eventCategoryOverride: undefined,
|
||||
tiebreakerField: undefined,
|
||||
falsePositives: ['false positive 1', 'false positive 2'],
|
||||
from: 'now-6m',
|
||||
query: undefined,
|
||||
|
@ -128,7 +132,9 @@ export const getCreateThreatMatchRulesOptionsMock = (): CreateRulesOptions => ({
|
|||
concurrentSearches: undefined,
|
||||
description: 'some description',
|
||||
enabled: true,
|
||||
timestampField: undefined,
|
||||
eventCategoryOverride: undefined,
|
||||
tiebreakerField: undefined,
|
||||
exceptionsList: [],
|
||||
falsePositives: ['false positive 1', 'false positive 2'],
|
||||
filters: [],
|
||||
|
|
|
@ -29,7 +29,9 @@ export const createRules = async ({
|
|||
buildingBlockType,
|
||||
description,
|
||||
enabled,
|
||||
timestampField,
|
||||
eventCategoryOverride,
|
||||
tiebreakerField,
|
||||
falsePositives,
|
||||
from,
|
||||
query,
|
||||
|
@ -95,7 +97,9 @@ export const createRules = async ({
|
|||
description,
|
||||
ruleId,
|
||||
index,
|
||||
timestampField,
|
||||
eventCategoryOverride,
|
||||
tiebreakerField,
|
||||
falsePositives,
|
||||
from,
|
||||
immutable,
|
||||
|
|
|
@ -23,7 +23,9 @@ export const installPrepackagedRules = (
|
|||
building_block_type: buildingBlockType,
|
||||
description,
|
||||
enabled,
|
||||
timestamp_field: timestampField,
|
||||
event_category_override: eventCategoryOverride,
|
||||
tiebreaker_field: tiebreakerField,
|
||||
false_positives: falsePositives,
|
||||
from,
|
||||
query,
|
||||
|
@ -80,7 +82,9 @@ export const installPrepackagedRules = (
|
|||
buildingBlockType,
|
||||
description,
|
||||
enabled,
|
||||
timestampField,
|
||||
eventCategoryOverride,
|
||||
tiebreakerField,
|
||||
falsePositives,
|
||||
from,
|
||||
immutable: true, // At the moment we force all prepackaged rules to be immutable
|
||||
|
|
|
@ -18,7 +18,9 @@ export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({
|
|||
anomalyThreshold: undefined,
|
||||
description: 'some description',
|
||||
enabled: true,
|
||||
timestampField: undefined,
|
||||
eventCategoryOverride: undefined,
|
||||
tiebreakerField: undefined,
|
||||
falsePositives: ['false positive 1', 'false positive 2'],
|
||||
from: 'now-6m',
|
||||
query: 'user.name: root or user.name: admin',
|
||||
|
@ -70,7 +72,9 @@ export const getPatchMlRulesOptionsMock = (): PatchRulesOptions => ({
|
|||
anomalyThreshold: 55,
|
||||
description: 'some description',
|
||||
enabled: true,
|
||||
timestampField: undefined,
|
||||
eventCategoryOverride: undefined,
|
||||
tiebreakerField: undefined,
|
||||
falsePositives: ['false positive 1', 'false positive 2'],
|
||||
from: 'now-6m',
|
||||
query: undefined,
|
||||
|
|
|
@ -38,7 +38,9 @@ export const patchRules = async ({
|
|||
author,
|
||||
buildingBlockType,
|
||||
description,
|
||||
timestampField,
|
||||
eventCategoryOverride,
|
||||
tiebreakerField,
|
||||
falsePositives,
|
||||
enabled,
|
||||
query,
|
||||
|
@ -96,7 +98,9 @@ export const patchRules = async ({
|
|||
author,
|
||||
buildingBlockType,
|
||||
description,
|
||||
timestampField,
|
||||
eventCategoryOverride,
|
||||
tiebreakerField,
|
||||
falsePositives,
|
||||
query,
|
||||
language,
|
||||
|
@ -151,6 +155,9 @@ export const patchRules = async ({
|
|||
author,
|
||||
buildingBlockType,
|
||||
description,
|
||||
timestampField,
|
||||
eventCategoryOverride,
|
||||
tiebreakerField,
|
||||
falsePositives,
|
||||
from,
|
||||
query,
|
||||
|
|
|
@ -91,7 +91,9 @@ import {
|
|||
TimestampOverrideOrUndefined,
|
||||
BuildingBlockTypeOrUndefined,
|
||||
RuleNameOverrideOrUndefined,
|
||||
TimestampFieldOrUndefined,
|
||||
EventCategoryOverrideOrUndefined,
|
||||
TiebreakerFieldOrUndefined,
|
||||
NamespaceOrUndefined,
|
||||
RelatedIntegrationArray,
|
||||
RequiredFieldArray,
|
||||
|
@ -147,7 +149,9 @@ export interface CreateRulesOptions {
|
|||
buildingBlockType: BuildingBlockTypeOrUndefined;
|
||||
description: Description;
|
||||
enabled: Enabled;
|
||||
timestampField: TimestampFieldOrUndefined;
|
||||
eventCategoryOverride: EventCategoryOverrideOrUndefined;
|
||||
tiebreakerField: TiebreakerFieldOrUndefined;
|
||||
falsePositives: FalsePositives;
|
||||
from: From;
|
||||
query: QueryOrUndefined;
|
||||
|
@ -216,7 +220,9 @@ interface PatchRulesFieldsOptions {
|
|||
buildingBlockType: BuildingBlockTypeOrUndefined;
|
||||
description: DescriptionOrUndefined;
|
||||
enabled: EnabledOrUndefined;
|
||||
timestampField: TimestampFieldOrUndefined;
|
||||
eventCategoryOverride: EventCategoryOverrideOrUndefined;
|
||||
tiebreakerField: TiebreakerFieldOrUndefined;
|
||||
falsePositives: FalsePositivesOrUndefined;
|
||||
from: FromOrUndefined;
|
||||
query: QueryOrUndefined;
|
||||
|
|
|
@ -70,7 +70,9 @@ export const createPromises = (
|
|||
author,
|
||||
building_block_type: buildingBlockType,
|
||||
description,
|
||||
timestamp_field: timestampField,
|
||||
event_category_override: eventCategoryOverride,
|
||||
tiebreaker_field: tiebreakerField,
|
||||
false_positives: falsePositives,
|
||||
from,
|
||||
query,
|
||||
|
@ -154,7 +156,9 @@ export const createPromises = (
|
|||
buildingBlockType,
|
||||
description,
|
||||
enabled: migratedRule.enabled, // Enabled comes from existing rule
|
||||
timestampField,
|
||||
eventCategoryOverride,
|
||||
tiebreakerField,
|
||||
falsePositives,
|
||||
from,
|
||||
immutable: true, // At the moment we force all prepackaged rules to be immutable
|
||||
|
@ -212,7 +216,9 @@ export const createPromises = (
|
|||
author,
|
||||
buildingBlockType,
|
||||
description,
|
||||
timestampField,
|
||||
eventCategoryOverride,
|
||||
tiebreakerField,
|
||||
falsePositives,
|
||||
from,
|
||||
query,
|
||||
|
|
|
@ -112,7 +112,9 @@ describe('utils', () => {
|
|||
author: [],
|
||||
buildingBlockType: undefined,
|
||||
description: 'some description change',
|
||||
timestampField: undefined,
|
||||
eventCategoryOverride: undefined,
|
||||
tiebreakerField: undefined,
|
||||
falsePositives: undefined,
|
||||
query: undefined,
|
||||
language: undefined,
|
||||
|
@ -167,7 +169,9 @@ describe('utils', () => {
|
|||
author: [],
|
||||
buildingBlockType: undefined,
|
||||
description: 'some description change',
|
||||
timestampField: undefined,
|
||||
eventCategoryOverride: undefined,
|
||||
tiebreakerField: undefined,
|
||||
falsePositives: undefined,
|
||||
query: undefined,
|
||||
language: undefined,
|
||||
|
@ -222,7 +226,9 @@ describe('utils', () => {
|
|||
author: [],
|
||||
buildingBlockType: undefined,
|
||||
description: 'some description change',
|
||||
timestampField: undefined,
|
||||
eventCategoryOverride: undefined,
|
||||
tiebreakerField: undefined,
|
||||
falsePositives: undefined,
|
||||
query: undefined,
|
||||
language: undefined,
|
||||
|
|
|
@ -54,7 +54,9 @@ import {
|
|||
LicenseOrUndefined,
|
||||
RuleNameOverrideOrUndefined,
|
||||
TimestampOverrideOrUndefined,
|
||||
TimestampFieldOrUndefined,
|
||||
EventCategoryOverrideOrUndefined,
|
||||
TiebreakerFieldOrUndefined,
|
||||
NamespaceOrUndefined,
|
||||
RelatedIntegrationArray,
|
||||
RequiredFieldArray,
|
||||
|
@ -95,7 +97,9 @@ export interface UpdateProperties {
|
|||
author: AuthorOrUndefined;
|
||||
buildingBlockType: BuildingBlockTypeOrUndefined;
|
||||
description: DescriptionOrUndefined;
|
||||
timestampField: TimestampFieldOrUndefined;
|
||||
eventCategoryOverride: EventCategoryOverrideOrUndefined;
|
||||
tiebreakerField: TiebreakerFieldOrUndefined;
|
||||
falsePositives: FalsePositivesOrUndefined;
|
||||
from: FromOrUndefined;
|
||||
query: QueryOrUndefined;
|
||||
|
|
|
@ -58,7 +58,9 @@ export const typeSpecificSnakeToCamel = (params: CreateTypeSpecific): TypeSpecif
|
|||
index: params.index,
|
||||
query: params.query,
|
||||
filters: params.filters,
|
||||
timestampField: params.timestamp_field,
|
||||
eventCategoryOverride: params.event_category_override,
|
||||
tiebreakerField: params.tiebreaker_field,
|
||||
};
|
||||
}
|
||||
case 'threat_match': {
|
||||
|
@ -184,7 +186,9 @@ export const typeSpecificCamelToSnake = (params: TypeSpecificRuleParams): Respon
|
|||
index: params.index,
|
||||
query: params.query,
|
||||
filters: params.filters,
|
||||
timestamp_field: params.timestampField,
|
||||
event_category_override: params.eventCategoryOverride,
|
||||
tiebreaker_field: params.tiebreakerField,
|
||||
};
|
||||
}
|
||||
case 'threat_match': {
|
||||
|
|
|
@ -87,7 +87,9 @@ export const getEqlRuleParams = (): EqlRuleParams => {
|
|||
index: ['some-index'],
|
||||
query: 'any where true',
|
||||
filters: undefined,
|
||||
timestampField: undefined,
|
||||
eventCategoryOverride: undefined,
|
||||
tiebreakerField: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -63,7 +63,9 @@ import {
|
|||
timestampOverrideOrUndefined,
|
||||
to,
|
||||
references,
|
||||
timestampFieldOrUndefined,
|
||||
eventCategoryOverrideOrUndefined,
|
||||
tiebreakerFieldOrUndefined,
|
||||
savedIdOrUndefined,
|
||||
saved_id,
|
||||
thresholdNormalized,
|
||||
|
@ -121,7 +123,9 @@ const eqlSpecificRuleParams = t.type({
|
|||
index: indexOrUndefined,
|
||||
query,
|
||||
filters: filtersOrUndefined,
|
||||
timestampField: timestampFieldOrUndefined,
|
||||
eventCategoryOverride: eventCategoryOverrideOrUndefined,
|
||||
tiebreakerField: tiebreakerFieldOrUndefined,
|
||||
});
|
||||
export const eqlRuleParams = t.intersection([baseRuleParams, eqlSpecificRuleParams]);
|
||||
export type EqlRuleParams = t.TypeOf<typeof eqlRuleParams>;
|
||||
|
|
|
@ -175,7 +175,9 @@ export const buildEqlSearchRequest = (
|
|||
size: number,
|
||||
timestampOverride: TimestampOverrideOrUndefined,
|
||||
exceptionLists: ExceptionListItemSchema[],
|
||||
eventCategoryOverride: string | undefined
|
||||
eventCategoryOverride?: string,
|
||||
timestampField?: string,
|
||||
tiebreakerField?: string
|
||||
): estypes.EqlSearchRequest => {
|
||||
const defaultTimeFields = ['@timestamp'];
|
||||
const timestamps =
|
||||
|
@ -225,7 +227,9 @@ export const buildEqlSearchRequest = (
|
|||
filter: requestFilter,
|
||||
},
|
||||
},
|
||||
timestamp_field: timestampField,
|
||||
event_category_field: eventCategoryOverride,
|
||||
tiebreaker_field: tiebreakerField,
|
||||
fields,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -84,7 +84,9 @@ export const eqlExecutor = async ({
|
|||
completeRule.ruleParams.maxSignals,
|
||||
ruleParams.timestampOverride,
|
||||
exceptionItems,
|
||||
ruleParams.eventCategoryOverride
|
||||
ruleParams.eventCategoryOverride,
|
||||
ruleParams.timestampField,
|
||||
ruleParams.tiebreakerField
|
||||
);
|
||||
|
||||
const eqlSignalSearchStart = performance.now();
|
||||
|
|
|
@ -51,7 +51,9 @@ import {
|
|||
LicenseOrUndefined,
|
||||
RuleNameOverrideOrUndefined,
|
||||
TimestampOverrideOrUndefined,
|
||||
TimestampFieldOrUndefined,
|
||||
EventCategoryOverrideOrUndefined,
|
||||
TiebreakerFieldOrUndefined,
|
||||
} from '../../../common/detection_engine/schemas/common/schemas';
|
||||
|
||||
export type PartialFilter = Partial<Filter>;
|
||||
|
@ -62,7 +64,9 @@ export interface RuleTypeParams extends AlertingRuleTypeParams {
|
|||
buildingBlockType: BuildingBlockTypeOrUndefined;
|
||||
description: Description;
|
||||
note: NoteOrUndefined;
|
||||
timestampField?: TimestampFieldOrUndefined;
|
||||
eventCategoryOverride?: EventCategoryOverrideOrUndefined;
|
||||
tiebreakerField?: TiebreakerFieldOrUndefined;
|
||||
falsePositives: FalsePositives;
|
||||
from: From;
|
||||
ruleId: RuleId;
|
||||
|
|
|
@ -247,6 +247,18 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
|
||||
describe('EQL Rules', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load(
|
||||
'x-pack/test/functional/es_archives/security_solution/timestamp_override_6'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload(
|
||||
'x-pack/test/functional/es_archives/security_solution/timestamp_override_6'
|
||||
);
|
||||
});
|
||||
|
||||
it('generates a correctly formatted signal from EQL non-sequence queries', async () => {
|
||||
const rule: EqlCreateSchema = {
|
||||
...getEqlRuleForSignalTesting(['auditbeat-*']),
|
||||
|
@ -436,6 +448,38 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
});
|
||||
|
||||
it('uses the provided timestamp_field', async () => {
|
||||
const rule: EqlCreateSchema = {
|
||||
...getEqlRuleForSignalTesting(['fake.index.1']),
|
||||
query: 'any where true',
|
||||
timestamp_field: 'created_at',
|
||||
};
|
||||
const { id } = await createRule(supertest, log, rule);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
await waitForSignalsToBePresent(supertest, log, 1, [id]);
|
||||
const signals = await getSignalsByIds(supertest, log, [id]);
|
||||
expect(signals.hits.hits.length).eql(3);
|
||||
|
||||
const createdAtHits = signals.hits.hits.map((hit) => hit._source?.created_at);
|
||||
expect(createdAtHits).to.eql([1622676785, 1622676790, 1622676795]);
|
||||
});
|
||||
|
||||
it('uses the provided tiebreaker_field', async () => {
|
||||
const rule: EqlCreateSchema = {
|
||||
...getEqlRuleForSignalTesting(['fake.index.1']),
|
||||
query: 'any where true',
|
||||
tiebreaker_field: 'locale',
|
||||
};
|
||||
const { id } = await createRule(supertest, log, rule);
|
||||
await waitForRuleSuccessOrStatus(supertest, log, id);
|
||||
await waitForSignalsToBePresent(supertest, log, 1, [id]);
|
||||
const signals = await getSignalsByIds(supertest, log, [id]);
|
||||
expect(signals.hits.hits.length).eql(3);
|
||||
|
||||
const createdAtHits = signals.hits.hits.map((hit) => hit._source?.locale);
|
||||
expect(createdAtHits).to.eql(['es', 'pt', 'ua']);
|
||||
});
|
||||
|
||||
it('generates building block signals from EQL sequences in the expected form', async () => {
|
||||
const rule: EqlCreateSchema = {
|
||||
...getEqlRuleForSignalTesting(['auditbeat-*']),
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": "fake.index.1",
|
||||
"source": {
|
||||
"@timestamp": 1608131778,
|
||||
"locale": "pt",
|
||||
"created_at": 1622676795
|
||||
},
|
||||
"type": "_doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": "fake.index.1",
|
||||
"source": {
|
||||
"@timestamp": 1608131778,
|
||||
"locale": "es",
|
||||
"created_at": 1622676790
|
||||
},
|
||||
"type": "_doc"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": "fake.index.1",
|
||||
"source": {
|
||||
"@timestamp": 1608131779,
|
||||
"locale": "ua",
|
||||
"created_at": 1622676785
|
||||
},
|
||||
"type": "_doc"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": "fake.index.1",
|
||||
"mappings": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"@timestamp": {
|
||||
"type": "date",
|
||||
"format": "epoch_second"
|
||||
},
|
||||
"locale": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "date",
|
||||
"format": "epoch_second"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"index": {
|
||||
"refresh_interval": "1s",
|
||||
"number_of_replicas": "1",
|
||||
"number_of_shards": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue