[Security Solution] Expose EQL search API configs in EQL rules #130339 (#132247)

* 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:
Ievgen Sorokopud 2022-05-24 21:28:11 +02:00 committed by GitHub
parent 16e12a20bb
commit 1a90718596
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 536 additions and 33 deletions

View file

@ -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>;

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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: {},
};

View file

@ -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);
});
});
});

View file

@ -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 })),
];

View file

@ -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;

View file

@ -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) });

View file

@ -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',
{

View file

@ -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}

View file

@ -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}

View file

@ -55,6 +55,7 @@ const defaultProps: RulePreviewProps = {
},
anomalyThreshold: 50,
machineLearningJobId: ['test-ml-job-id'],
eqlOptions: {},
};
describe('PreviewQuery', () => {

View file

@ -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

View file

@ -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 {

View file

@ -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>

View file

@ -77,6 +77,7 @@ export const schema: FormSchema<DefineStepRule> = {
},
],
},
eqlOptions: {},
queryBar: {
validations: [
{

View file

@ -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,

View file

@ -211,6 +211,7 @@ export const mockDefineStepRule = (): DefineStepRule => ({
value: '2',
},
},
eqlOptions: {},
});
export const mockScheduleStepRule = (): ScheduleStepRule => ({

View file

@ -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'] = [
{

View file

@ -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,

View file

@ -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);

View file

@ -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 => {

View file

@ -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 {

View file

@ -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,

View file

@ -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');

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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: [],

View file

@ -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: [],

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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;

View file

@ -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,

View file

@ -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,

View file

@ -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;

View file

@ -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': {

View file

@ -87,7 +87,9 @@ export const getEqlRuleParams = (): EqlRuleParams => {
index: ['some-index'],
query: 'any where true',
filters: undefined,
timestampField: undefined,
eventCategoryOverride: undefined,
tiebreakerField: undefined,
};
};

View file

@ -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>;

View file

@ -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,
},
};

View file

@ -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();

View file

@ -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;

View file

@ -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-*']),

View file

@ -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"
}
}

View file

@ -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"
}
}
}
}