[Security Solution][Detection Engine] Convert wrapHits from factory to regular function (#213826)

## Summary

Follow up to https://github.com/elastic/kibana/pull/212694. This PR
switches more DE functions over to use `sharedParams`, an object that
contains the variety of parameters that we build at the top level of
rule execution in the common security rule wrapper. With `sharedParams`
available throughout rule execution, it's easier to access all the
parameters necessary for `wrapHits` when we call it so I also removed
the "factory" logic and just call `wrapHits` directly instead of passing
the function as a parameter on `sharedParams`.

There should be very few behavior changes in the code as a result of
this PR.
- `kibana.alert.rule.indices` is now populated for ES|QL alerts
- `ignoreFields` and `ignoreFieldsRegexes` are now respected by EQL, new
terms, and all suppressed rule types

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marshall Main 2025-03-14 12:12:50 -04:00 committed by GitHub
parent 02409dbd65
commit 920ce1b9ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 419 additions and 914 deletions

View file

@ -38,7 +38,6 @@ export const getSharedParamsMock = <T extends RuleParams = QueryRuleParams>({
},
searchAfterSize: 100,
bulkCreate: jest.fn(),
wrapHits: jest.fn(),
ruleDataClient: createRuleDataClientMock(),
runtimeMappings: undefined,
aggregatableTimestampField: '@timestamp',
@ -46,5 +45,7 @@ export const getSharedParamsMock = <T extends RuleParams = QueryRuleParams>({
exceptionFilter: undefined,
alertWithSuppression: jest.fn(),
refreshOnIndexingAlerts: false,
ignoreFields: {},
ignoreFieldsRegexes: [],
...rewrites,
});

View file

@ -37,7 +37,7 @@ import { getNotificationResultsLink } from '../rule_actions_legacy';
// eslint-disable-next-line no-restricted-imports
import { formatAlertForNotificationActions } from '../rule_actions_legacy/logic/notifications/schedule_notification_actions';
import { createResultObject } from './utils';
import { bulkCreateFactory, wrapHitsFactory } from './factories';
import { bulkCreateFactory } from './factories';
import { RuleExecutionStatusEnum } from '../../../../common/api/detection_engine/rule_monitoring';
import { truncateList } from '../rule_monitoring';
import aadFieldConversion from '../routes/index/signal_aad_mapping.json';
@ -421,18 +421,6 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
});
const intendedTimestamp = startedAtOverridden ? startedAt : undefined;
const wrapHits = wrapHitsFactory({
ignoreFields: ignoreFieldsObject,
ignoreFieldsRegexes,
mergeStrategy,
completeRule,
spaceId,
indicesToQuery: inputIndex,
alertTimestampOverride,
publicBaseUrl,
ruleExecutionLogger,
intendedTimestamp,
});
const { filter: exceptionFilter, unprocessedExceptions } = await buildExceptionFilter({
startedAt,
@ -461,7 +449,6 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
searchAfterSize,
tuple,
bulkCreate,
wrapHits,
listClient,
ruleDataClient,
mergeStrategy,
@ -476,6 +463,8 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
experimentalFeatures,
intendedTimestamp,
spaceId,
ignoreFields: ignoreFieldsObject,
ignoreFieldsRegexes,
},
});

View file

@ -76,7 +76,6 @@ export const buildAlertGroupFromSequence = ({
const {
alertTimestampOverride,
intendedTimestamp,
mergeStrategy,
completeRule,
spaceId,
inputIndex: indicesToQuery,
@ -96,21 +95,11 @@ export const buildAlertGroupFromSequence = ({
try {
baseAlerts = sequence.events.map((event) =>
transformHitToAlert({
spaceId,
completeRule,
sharedParams,
doc: event,
mergeStrategy,
// TODO: respect ignore fields
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: false,
buildReasonMessage,
indicesToQuery,
alertTimestampOverride,
ruleExecutionLogger,
alertUuid: 'placeholder-alert-uuid', // This is overriden below
publicBaseUrl,
intendedTimestamp,
})
);
} catch (error) {

View file

@ -53,6 +53,7 @@ import { isEqlSequenceQuery } from '../../../../../common/detection_engine/utils
import { logShardFailures } from '../utils/log_shard_failure';
import { checkErrorDetails } from '../utils/check_error_details';
import { wrapSequences } from './wrap_sequences';
import { wrapHits } from '../factories';
interface EqlExecutorParams {
sharedParams: SecuritySharedParams<EqlRuleParams>;
@ -76,7 +77,7 @@ export const eqlExecutor = async ({
result: SearchAfterAndBulkCreateReturnType;
loggedRequests?: RulePreviewLoggedRequest[];
}> => {
const { completeRule, tuple, ruleExecutionLogger, bulkCreate, wrapHits } = sharedParams;
const { completeRule, tuple, ruleExecutionLogger, bulkCreate } = sharedParams;
const ruleParams = completeRule.ruleParams;
const isLoggedRequestsEnabled = state?.isLoggedRequestsEnabled ?? false;
const loggedRequests: RulePreviewLoggedRequest[] = [];
@ -157,7 +158,7 @@ export const eqlExecutor = async ({
experimentalFeatures,
});
} else {
newSignals = wrapHits(events, buildReasonMessageForEqlAlert);
newSignals = wrapHits(sharedParams, events, buildReasonMessageForEqlAlert);
}
} else if (sequences) {
if (

View file

@ -71,11 +71,6 @@ export const esqlExecutor = async ({
exceptionFilter,
unprocessedExceptions,
ruleExecutionLogger,
spaceId,
mergeStrategy,
alertTimestampOverride,
publicBaseUrl,
intendedTimestamp,
bulkCreate,
} = sharedParams;
const loggedRequests: RulePreviewLoggedRequest[] = [];
@ -168,21 +163,6 @@ export const esqlExecutor = async ({
completeRule.ruleParams.query
);
const wrapHits = (events: Array<estypes.SearchHit<SignalSource>>) =>
wrapEsqlAlerts({
events,
spaceId,
completeRule,
mergeStrategy,
isRuleAggregating,
alertTimestampOverride,
ruleExecutionLogger,
publicBaseUrl,
tuple,
intendedTimestamp,
expandedFields,
});
const syntheticHits: Array<estypes.SearchHit<SignalSource>> = results.map((document) => {
const { _id, _version, _index, ...esqlResult } = document;
@ -202,18 +182,9 @@ export const esqlExecutor = async ({
if (isAlertSuppressionActive) {
const wrapSuppressedHits = (events: Array<estypes.SearchHit<SignalSource>>) =>
wrapSuppressedEsqlAlerts({
sharedParams,
events,
spaceId,
completeRule,
mergeStrategy,
isRuleAggregating,
alertTimestampOverride,
ruleExecutionLogger,
publicBaseUrl,
primaryTimestamp,
secondaryTimestamp,
tuple,
intendedTimestamp,
expandedFields,
});
@ -240,7 +211,12 @@ export const esqlExecutor = async ({
break;
}
} else {
const wrappedAlerts = wrapHits(syntheticHits);
const wrappedAlerts = wrapEsqlAlerts({
sharedParams,
events: syntheticHits,
isRuleAggregating,
expandedFields,
});
const enrichAlerts = createEnrichEventsFunction({
services,

View file

@ -4,47 +4,29 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { cloneDeep } from 'lodash';
import moment from 'moment';
import { ALERT_UUID } from '@kbn/rule-data-utils';
import { getCompleteRuleMock, getEsqlRuleParams } from '../../rule_schema/mocks';
import { ruleExecutionLogMock } from '../../rule_monitoring/mocks';
import { getEsqlRuleParams } from '../../rule_schema/mocks';
import { sampleDocNoSortIdWithTimestamp } from '../__mocks__/es_results';
import { wrapEsqlAlerts } from './wrap_esql_alerts';
import * as esqlUtils from './utils/generate_alert_id';
const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create();
import { getSharedParamsMock } from '../__mocks__/shared_params';
const docId = 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71';
const publicBaseUrl = 'http://somekibanabaseurl.com';
const alertSuppression = {
groupBy: ['source.ip'],
};
const completeRule = getCompleteRuleMock(getEsqlRuleParams());
completeRule.ruleParams.alertSuppression = alertSuppression;
const sharedParams = getSharedParamsMock({
ruleParams: getEsqlRuleParams(),
});
describe('wrapSuppressedEsqlAlerts', () => {
test('should create an alert with the correct _id from a document', () => {
const doc = sampleDocNoSortIdWithTimestamp(docId);
const alerts = wrapEsqlAlerts({
sharedParams,
events: [doc],
isRuleAggregating: false,
spaceId: 'default',
mergeStrategy: 'missingFields',
completeRule,
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl,
tuple: {
to: moment('2010-10-20 04:43:12'),
from: moment('2010-10-20 04:43:12'),
maxSignals: 100,
},
intendedTimestamp: undefined,
expandedFields: undefined,
});
expect(alerts[0]._id).toEqual('ed7fbf575371c898e0f0aea48cdf0bf1865939a9');
@ -53,26 +35,19 @@ describe('wrapSuppressedEsqlAlerts', () => {
test('should call generateAlertId for alert id', () => {
jest.spyOn(esqlUtils, 'generateAlertId').mockReturnValueOnce('mocked-alert-id');
const completeRuleCloned = cloneDeep(completeRule);
completeRuleCloned.ruleParams.alertSuppression = {
groupBy: ['someKey'],
};
const newSharedParams = getSharedParamsMock({
ruleParams: getEsqlRuleParams({
alertSuppression: {
groupBy: ['someKey'],
},
}),
});
const doc = sampleDocNoSortIdWithTimestamp(docId);
const alerts = wrapEsqlAlerts({
sharedParams: newSharedParams,
events: [doc],
spaceId: 'default',
isRuleAggregating: true,
mergeStrategy: 'missingFields',
completeRule: completeRuleCloned,
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl,
tuple: {
to: moment('2010-10-20 04:43:12'),
from: moment('2010-10-20 04:43:12'),
maxSignals: 100,
},
intendedTimestamp: undefined,
expandedFields: undefined,
});
expect(alerts[0]._id).toEqual('mocked-alert-id');
@ -87,7 +62,7 @@ describe('wrapSuppressedEsqlAlerts', () => {
spaceId: 'default',
tuple: {
from: expect.any(Object),
maxSignals: 100,
maxSignals: 10000,
to: expect.any(Object),
},
})
@ -99,20 +74,10 @@ describe('wrapSuppressedEsqlAlerts', () => {
const doc2 = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d72');
const doc3 = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d73');
const alerts = wrapEsqlAlerts({
sharedParams,
events: [doc1, doc1, doc2, doc2, doc3],
isRuleAggregating: false,
spaceId: 'default',
mergeStrategy: 'missingFields',
completeRule,
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl,
tuple: {
to: moment('2010-10-20 04:43:12'),
from: moment('2010-10-20 04:43:12'),
maxSignals: 100,
},
intendedTimestamp: undefined,
expandedFields: undefined,
});
expect(alerts).toHaveLength(3);

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import type { Moment } from 'moment';
import type { estypes } from '@elastic/elasticsearch';
import { uniqBy } from 'lodash';
@ -13,69 +12,40 @@ import type {
BaseFieldsLatest,
WrappedFieldsLatest,
} from '../../../../../common/api/detection_engine/model/alerts';
import type { ConfigType } from '../../../../config';
import type { CompleteRule, EsqlRuleParams } from '../../rule_schema';
import type { EsqlRuleParams } from '../../rule_schema';
import { buildReasonMessageForNewTermsAlert } from '../utils/reason_formatters';
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
import { transformHitToAlert } from '../factories/utils/transform_hit_to_alert';
import type { SignalSource } from '../types';
import type { SecuritySharedParams, SignalSource } from '../types';
import { generateAlertId } from './utils';
export const wrapEsqlAlerts = ({
sharedParams,
events,
spaceId,
completeRule,
mergeStrategy,
alertTimestampOverride,
ruleExecutionLogger,
publicBaseUrl,
tuple,
isRuleAggregating,
intendedTimestamp,
expandedFields,
}: {
sharedParams: SecuritySharedParams<EsqlRuleParams>;
isRuleAggregating: boolean;
events: Array<estypes.SearchHit<SignalSource>>;
spaceId: string | null | undefined;
completeRule: CompleteRule<EsqlRuleParams>;
mergeStrategy: ConfigType['alertMergeStrategy'];
alertTimestampOverride: Date | undefined;
ruleExecutionLogger: IRuleExecutionLogForExecutors;
publicBaseUrl: string | undefined;
tuple: {
to: Moment;
from: Moment;
maxSignals: number;
};
intendedTimestamp: Date | undefined;
expandedFields?: string[];
expandedFields: string[] | undefined;
}): Array<WrappedFieldsLatest<BaseFieldsLatest>> => {
const wrapped = events.map<WrappedFieldsLatest<BaseFieldsLatest>>((event, i) => {
const id = generateAlertId({
event,
spaceId,
completeRule,
tuple,
spaceId: sharedParams.spaceId,
completeRule: sharedParams.completeRule,
tuple: sharedParams.tuple,
isRuleAggregating,
index: i,
expandedFields,
});
const baseAlert: BaseFieldsLatest = transformHitToAlert({
spaceId,
completeRule,
sharedParams,
doc: event,
mergeStrategy,
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage: buildReasonMessageForNewTermsAlert,
indicesToQuery: [],
alertTimestampOverride,
ruleExecutionLogger,
alertUuid: id,
publicBaseUrl,
intendedTimestamp,
});
return {

View file

@ -4,8 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { cloneDeep } from 'lodash';
import moment from 'moment';
import {
ALERT_URL,
@ -16,14 +14,12 @@ import {
ALERT_SUPPRESSION_START,
ALERT_SUPPRESSION_END,
} from '@kbn/rule-data-utils';
import { getCompleteRuleMock, getEsqlRuleParams } from '../../rule_schema/mocks';
import { ruleExecutionLogMock } from '../../rule_monitoring/mocks';
import { getEsqlRuleParams } from '../../rule_schema/mocks';
import { sampleDocNoSortIdWithTimestamp } from '../__mocks__/es_results';
import { wrapSuppressedEsqlAlerts } from './wrap_suppressed_esql_alerts';
import * as esqlUtils from './utils/generate_alert_id';
const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create();
import { getSharedParamsMock } from '../__mocks__/shared_params';
const docId = 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71';
const publicBaseUrl = 'http://somekibanabaseurl.com';
@ -32,28 +28,19 @@ const alertSuppression = {
groupBy: ['source.ip'],
};
const completeRule = getCompleteRuleMock(getEsqlRuleParams());
completeRule.ruleParams.alertSuppression = alertSuppression;
const sharedParams = getSharedParamsMock({
ruleParams: getEsqlRuleParams({ alertSuppression }),
rewrites: { publicBaseUrl },
});
describe('wrapSuppressedEsqlAlerts', () => {
test('should create an alert with the correct _id from a document and suppression fields', () => {
const doc = sampleDocNoSortIdWithTimestamp(docId);
const alerts = wrapSuppressedEsqlAlerts({
sharedParams,
events: [doc],
isRuleAggregating: false,
spaceId: 'default',
mergeStrategy: 'missingFields',
completeRule,
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl,
primaryTimestamp: '@timestamp',
tuple: {
to: moment('2010-10-20 04:43:12'),
from: moment('2010-10-20 04:43:12'),
maxSignals: 100,
},
intendedTimestamp: undefined,
expandedFields: undefined,
});
expect(alerts[0]._id).toEqual('ed7fbf575371c898e0f0aea48cdf0bf1865939a9');
@ -73,27 +60,20 @@ describe('wrapSuppressedEsqlAlerts', () => {
});
test('should create an alert with a different _id if suppression field is different', () => {
const completeRuleCloned = cloneDeep(completeRule);
completeRuleCloned.ruleParams.alertSuppression = {
groupBy: ['someKey'],
};
const newSharedParams = getSharedParamsMock({
ruleParams: getEsqlRuleParams({
alertSuppression: {
groupBy: ['someKey'],
},
}),
rewrites: { publicBaseUrl },
});
const doc = sampleDocNoSortIdWithTimestamp(docId);
const alerts = wrapSuppressedEsqlAlerts({
sharedParams: newSharedParams,
events: [doc],
spaceId: 'default',
isRuleAggregating: true,
mergeStrategy: 'missingFields',
completeRule: completeRuleCloned,
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl,
primaryTimestamp: '@timestamp',
tuple: {
to: moment('2010-10-20 04:43:12'),
from: moment('2010-10-20 04:43:12'),
maxSignals: 100,
},
intendedTimestamp: undefined,
expandedFields: undefined,
});
expect(alerts[0]._source[ALERT_URL]).toContain(
@ -110,27 +90,12 @@ describe('wrapSuppressedEsqlAlerts', () => {
test('should call generateAlertId for alert id', () => {
jest.spyOn(esqlUtils, 'generateAlertId').mockReturnValueOnce('mocked-alert-id');
const completeRuleCloned = cloneDeep(completeRule);
completeRuleCloned.ruleParams.alertSuppression = {
groupBy: ['someKey'],
};
const doc = sampleDocNoSortIdWithTimestamp(docId);
const alerts = wrapSuppressedEsqlAlerts({
sharedParams,
events: [doc],
spaceId: 'default',
isRuleAggregating: false,
mergeStrategy: 'missingFields',
completeRule: completeRuleCloned,
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl,
primaryTimestamp: '@timestamp',
tuple: {
to: moment('2010-10-20 04:43:12'),
from: moment('2010-10-20 04:43:12'),
maxSignals: 100,
},
intendedTimestamp: undefined,
expandedFields: undefined,
});
expect(alerts[0]._id).toEqual('mocked-alert-id');
@ -145,7 +110,7 @@ describe('wrapSuppressedEsqlAlerts', () => {
spaceId: 'default',
tuple: {
from: expect.any(Object),
maxSignals: 100,
maxSignals: 10000,
to: expect.any(Object),
},
})
@ -156,21 +121,10 @@ describe('wrapSuppressedEsqlAlerts', () => {
const doc1 = sampleDocNoSortIdWithTimestamp(docId);
const doc2 = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d72');
const alerts = wrapSuppressedEsqlAlerts({
sharedParams,
events: [doc1, doc1, doc2],
isRuleAggregating: false,
spaceId: 'default',
mergeStrategy: 'missingFields',
completeRule,
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl,
primaryTimestamp: '@timestamp',
tuple: {
to: moment('2010-10-20 04:43:12'),
from: moment('2010-10-20 04:43:12'),
maxSignals: 100,
},
intendedTimestamp: undefined,
expandedFields: undefined,
});
expect(alerts).toHaveLength(2);

View file

@ -7,7 +7,6 @@
import { uniqBy } from 'lodash';
import objectHash from 'object-hash';
import type { Moment } from 'moment';
import type { estypes } from '@elastic/elasticsearch';
import { TIMESTAMP } from '@kbn/rule-data-utils';
import type { SuppressionFieldsLatest } from '@kbn/rule-registry-plugin/common/schemas';
@ -16,48 +15,25 @@ import type {
BaseFieldsLatest,
WrappedFieldsLatest,
} from '../../../../../common/api/detection_engine/model/alerts';
import type { ConfigType } from '../../../../config';
import type { CompleteRule, EsqlRuleParams } from '../../rule_schema';
import type { EsqlRuleParams } from '../../rule_schema';
import { buildReasonMessageForNewTermsAlert } from '../utils/reason_formatters';
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
import { transformHitToAlert } from '../factories/utils/transform_hit_to_alert';
import type { SignalSource } from '../types';
import type { SecuritySharedParams, SignalSource } from '../types';
import { getSuppressionAlertFields, getSuppressionTerms } from '../utils';
import { generateAlertId } from './utils';
export const wrapSuppressedEsqlAlerts = ({
sharedParams,
events,
spaceId,
completeRule,
mergeStrategy,
alertTimestampOverride,
ruleExecutionLogger,
publicBaseUrl,
tuple,
isRuleAggregating,
primaryTimestamp,
secondaryTimestamp,
intendedTimestamp,
expandedFields,
}: {
sharedParams: SecuritySharedParams<EsqlRuleParams>;
isRuleAggregating: boolean;
events: Array<estypes.SearchHit<SignalSource>>;
spaceId: string | null | undefined;
completeRule: CompleteRule<EsqlRuleParams>;
mergeStrategy: ConfigType['alertMergeStrategy'];
alertTimestampOverride: Date | undefined;
ruleExecutionLogger: IRuleExecutionLogForExecutors;
publicBaseUrl: string | undefined;
tuple: {
to: Moment;
from: Moment;
maxSignals: number;
};
primaryTimestamp: string;
secondaryTimestamp?: string;
intendedTimestamp: Date | undefined;
expandedFields?: string[];
expandedFields: string[] | undefined;
}): Array<WrappedFieldsLatest<BaseFieldsLatest & SuppressionFieldsLatest>> => {
const { spaceId, completeRule, tuple, primaryTimestamp, secondaryTimestamp } = sharedParams;
const wrapped = events.map<WrappedFieldsLatest<BaseFieldsLatest & SuppressionFieldsLatest>>(
(event, i) => {
const combinedFields = { ...event?.fields, ...event._source };
@ -80,20 +56,11 @@ export const wrapSuppressedEsqlAlerts = ({
const instanceId = objectHash([suppressionTerms, completeRule.alertId, spaceId]);
const baseAlert: BaseFieldsLatest = transformHitToAlert({
spaceId,
completeRule,
sharedParams,
doc: event,
mergeStrategy,
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage: buildReasonMessageForNewTermsAlert,
indicesToQuery: [],
alertTimestampOverride,
ruleExecutionLogger,
alertUuid: id,
publicBaseUrl,
intendedTimestamp,
});
return {

View file

@ -6,5 +6,5 @@
*/
export * from './bulk_create_factory';
export * from './wrap_hits_factory';
export * from './wrap_hits';
export * from '../eql/wrap_sequences';

View file

@ -45,13 +45,10 @@ import {
} from '../../../../../../common/field_maps/field_names';
import { transformHitToAlert } from './transform_hit_to_alert';
import {
getCompleteRuleMock,
getEsqlRuleParams,
getQueryRuleParams,
} from '../../../rule_schema/mocks';
import { getEsqlRuleParams, getQueryRuleParams } from '../../../rule_schema/mocks';
import { ruleExecutionLogMock } from '../../../rule_monitoring/mocks';
import { get } from 'lodash';
import { getSharedParamsMock } from '../../__mocks__/shared_params';
const SPACE_ID = 'space';
const publicBaseUrl = 'testKibanaBasePath.com';
@ -66,23 +63,21 @@ describe('transformHitToAlert', () => {
'event.action': 'process',
'event.action.keyword': 'process',
});
const completeRule = getCompleteRuleMock(getEsqlRuleParams());
const sharedParams = getSharedParamsMock({
ruleParams: getEsqlRuleParams(),
rewrites: {
spaceId: SPACE_ID,
mergeStrategy: 'missingFields',
publicBaseUrl,
ruleExecutionLogger,
},
});
const alert = transformHitToAlert({
spaceId: SPACE_ID,
completeRule,
sharedParams,
doc,
mergeStrategy: 'missingFields',
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage: buildReasonMessageStub,
indicesToQuery: [],
alertTimestampOverride: undefined,
ruleExecutionLogger,
alertUuid,
publicBaseUrl,
intendedTimestamp: undefined,
});
expect(alert['kibana.alert.original_event.action']).toEqual('process');
@ -99,23 +94,23 @@ describe('transformHitToAlert', () => {
},
},
};
const completeRule = getCompleteRuleMock(getEsqlRuleParams());
const sharedParams = getSharedParamsMock({
ruleParams: getEsqlRuleParams(),
rewrites: {
spaceId: SPACE_ID,
mergeStrategy: 'missingFields',
publicBaseUrl,
ruleExecutionLogger,
},
});
const alert = transformHitToAlert({
spaceId: SPACE_ID,
completeRule,
sharedParams,
doc,
mergeStrategy: 'missingFields',
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage: buildReasonMessageStub,
indicesToQuery: [],
alertTimestampOverride: undefined,
ruleExecutionLogger,
alertUuid,
publicBaseUrl,
intendedTimestamp: undefined,
});
expect(get(alert.event, 'kind')).toEqual(undefined);
@ -130,23 +125,23 @@ describe('transformHitToAlert', () => {
'event.kind': 'test-value',
},
};
const completeRule = getCompleteRuleMock(getEsqlRuleParams());
const sharedParams = getSharedParamsMock({
ruleParams: getEsqlRuleParams(),
rewrites: {
spaceId: SPACE_ID,
mergeStrategy: 'missingFields',
publicBaseUrl,
ruleExecutionLogger,
},
});
const alert = transformHitToAlert({
spaceId: SPACE_ID,
completeRule,
sharedParams,
doc,
mergeStrategy: 'missingFields',
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage: buildReasonMessageStub,
indicesToQuery: [],
alertTimestampOverride: undefined,
ruleExecutionLogger,
alertUuid,
publicBaseUrl,
intendedTimestamp: undefined,
});
expect(get(alert.event, 'kind')).toEqual(undefined);
@ -161,23 +156,23 @@ describe('transformHitToAlert', () => {
testField: 'testValue',
},
};
const completeRule = getCompleteRuleMock(getEsqlRuleParams());
const sharedParams = getSharedParamsMock({
ruleParams: getEsqlRuleParams(),
rewrites: {
spaceId: SPACE_ID,
mergeStrategy: 'missingFields',
publicBaseUrl,
ruleExecutionLogger,
},
});
const alert = transformHitToAlert({
spaceId: SPACE_ID,
completeRule,
sharedParams,
doc,
mergeStrategy: 'missingFields',
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage: buildReasonMessageStub,
indicesToQuery: [],
alertTimestampOverride: undefined,
ruleExecutionLogger,
alertUuid,
publicBaseUrl,
intendedTimestamp: undefined,
});
expect(alert.event).toEqual(undefined);
@ -192,23 +187,23 @@ describe('transformHitToAlert', () => {
'event.action': ['process', { objectSubfield: 'test' }],
},
};
const completeRule = getCompleteRuleMock(getEsqlRuleParams());
const sharedParams = getSharedParamsMock({
ruleParams: getEsqlRuleParams(),
rewrites: {
spaceId: SPACE_ID,
mergeStrategy: 'missingFields',
publicBaseUrl,
ruleExecutionLogger,
},
});
const alert = transformHitToAlert({
spaceId: SPACE_ID,
completeRule,
sharedParams,
doc,
mergeStrategy: 'missingFields',
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage: buildReasonMessageStub,
indicesToQuery: [],
alertTimestampOverride: undefined,
ruleExecutionLogger,
alertUuid,
publicBaseUrl,
intendedTimestamp: undefined,
});
expect(alert['kibana.alert.original_event.action']).toEqual(['process']);
@ -216,22 +211,23 @@ describe('transformHitToAlert', () => {
it('builds an alert as expected without original_event if event does not exist', () => {
const doc = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71');
const completeRule = getCompleteRuleMock(getQueryRuleParams());
const sharedParams = getSharedParamsMock({
ruleParams: getQueryRuleParams(),
rewrites: {
spaceId: SPACE_ID,
mergeStrategy: 'missingFields',
publicBaseUrl,
ruleExecutionLogger,
inputIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
},
});
const alert = transformHitToAlert({
spaceId: SPACE_ID,
completeRule,
sharedParams,
doc,
mergeStrategy: 'missingFields',
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage: buildReasonMessageStub,
indicesToQuery: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
alertTimestampOverride: undefined,
ruleExecutionLogger,
alertUuid,
publicBaseUrl,
intendedTimestamp: undefined,
});
delete doc._source.event;
@ -329,7 +325,7 @@ describe('transformHitToAlert', () => {
filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }],
investigation_fields: undefined,
},
[ALERT_RULE_INDICES]: completeRule.ruleParams.index,
[ALERT_RULE_INDICES]: sharedParams.completeRule.ruleParams.index,
...flattenWithPrefix(ALERT_RULE_NAMESPACE, {
actions: [],
author: ['Elastic'],
@ -418,22 +414,21 @@ describe('transformHitToAlert', () => {
[EVENT_MODULE]: 'system',
},
};
const completeRule = getCompleteRuleMock(getQueryRuleParams());
const sharedParams = getSharedParamsMock({
ruleParams: getQueryRuleParams(),
rewrites: {
spaceId: SPACE_ID,
mergeStrategy: 'missingFields',
publicBaseUrl,
ruleExecutionLogger,
},
});
const alert = transformHitToAlert({
spaceId: SPACE_ID,
completeRule,
sharedParams,
doc,
mergeStrategy: 'missingFields',
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage: buildReasonMessageStub,
indicesToQuery: [],
alertTimestampOverride: undefined,
ruleExecutionLogger,
alertUuid,
publicBaseUrl,
intendedTimestamp: undefined,
});
const expected = {
...alert,
@ -459,22 +454,22 @@ describe('transformHitToAlert', () => {
[EVENT_MODULE]: 'system',
},
};
const completeRule = getCompleteRuleMock(getQueryRuleParams());
const sharedParams = getSharedParamsMock({
ruleParams: getQueryRuleParams(),
rewrites: {
spaceId: SPACE_ID,
mergeStrategy: 'missingFields',
publicBaseUrl,
ruleExecutionLogger,
intendedTimestamp: new Date('2019-01-01T00:00:00.000Z'),
},
});
const alert = transformHitToAlert({
spaceId: SPACE_ID,
completeRule,
sharedParams,
doc,
mergeStrategy: 'missingFields',
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage: buildReasonMessageStub,
indicesToQuery: [],
alertTimestampOverride: undefined,
ruleExecutionLogger,
alertUuid,
publicBaseUrl,
intendedTimestamp: new Date('2019-01-01T00:00:00.000Z'),
});
const expected = {
...alert,

View file

@ -11,13 +11,10 @@ import { requiredOptional } from '@kbn/zod-helpers';
import { EVENT_KIND } from '@kbn/rule-data-utils';
import type { BaseHit } from '../../../../../../common/detection_engine/types';
import type { ConfigType } from '../../../../../config';
import type { BuildReasonMessage } from '../../utils/reason_formatters';
import { getMergeStrategy } from '../../utils/source_fields_merging/strategies';
import type { SignalSource, SignalSourceHit } from '../../types';
import type { SecuritySharedParams, SignalSource, SignalSourceHit } from '../../types';
import { buildAlertFields, isThresholdResult } from './build_alert';
import type { CompleteRule, RuleParams } from '../../../rule_schema';
import type { IRuleExecutionLogForExecutors } from '../../../rule_monitoring';
import { buildRuleNameFromMapping } from '../../utils/mappings/build_rule_name_from_mapping';
import { buildSeverityFromMapping } from '../../utils/mappings/build_severity_from_mapping';
import { buildRiskScoreFromMapping } from '../../utils/mappings/build_risk_score_from_mapping';
@ -31,20 +28,11 @@ const isSourceDoc = (hit: SignalSourceHit): hit is BaseHit<SignalSource> => {
};
export interface TransformHitToAlertProps {
spaceId: string | null | undefined;
completeRule: CompleteRule<RuleParams>;
sharedParams: SecuritySharedParams;
doc: estypes.SearchHit<SignalSource>;
mergeStrategy: ConfigType['alertMergeStrategy'];
ignoreFields: Record<string, boolean>;
ignoreFieldsRegexes: string[];
applyOverrides: boolean;
buildReasonMessage: BuildReasonMessage;
indicesToQuery: string[];
alertTimestampOverride: Date | undefined;
ruleExecutionLogger: IRuleExecutionLogForExecutors;
alertUuid: string;
publicBaseUrl?: string;
intendedTimestamp: Date | undefined;
}
/**
@ -57,23 +45,27 @@ export interface TransformHitToAlertProps {
* @returns The body that can be added to a bulk call for inserting the signal.
*/
export const transformHitToAlert = ({
spaceId,
completeRule,
sharedParams,
doc,
mergeStrategy,
ignoreFields,
ignoreFieldsRegexes,
applyOverrides,
buildReasonMessage,
indicesToQuery,
alertTimestampOverride,
ruleExecutionLogger,
alertUuid,
publicBaseUrl,
intendedTimestamp,
}: TransformHitToAlertProps): BaseFieldsLatest => {
const mergedDoc = getMergeStrategy(mergeStrategy)({ doc, ignoreFields, ignoreFieldsRegexes });
const mergedDoc = getMergeStrategy(sharedParams.mergeStrategy)({
doc,
ignoreFields: sharedParams.ignoreFields,
ignoreFieldsRegexes: sharedParams.ignoreFieldsRegexes,
});
const thresholdResult = mergedDoc._source?.threshold_result;
const {
completeRule,
spaceId,
inputIndex: indicesToQuery,
publicBaseUrl,
alertTimestampOverride,
intendedTimestamp,
ruleExecutionLogger,
} = sharedParams;
if (isSourceDoc(mergedDoc)) {
const overrides = applyOverrides

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { estypes } from '@elastic/elasticsearch';
import type { SecuritySharedParams, SignalSource, SimpleHit } from '../types';
import { generateId } from '../utils/utils';
import { transformHitToAlert } from './utils/transform_hit_to_alert';
import type { BuildReasonMessage } from '../utils/reason_formatters';
import type {
BaseFieldsLatest,
WrappedFieldsLatest,
} from '../../../../../common/api/detection_engine/model/alerts';
/**
* wrapHits is responsible for turning source events into alerts. Since we copy the source data into the alert, we are
* effectively "wrapping" the source hits by adding alert metadata to them in `kibana.alert.*` fields and
* generating an _id for the alert,
* @param sharedParams SecuritySharedParams passed in from the common security rule wrapper logic
* @param events Source events to turn into alerts
* @param buildReasonMessage Function to generate the reason message based on source data
* @returns Alerts ready to index
*/
export const wrapHits = (
sharedParams: SecuritySharedParams,
events: Array<estypes.SearchHit<SignalSource>>,
buildReasonMessage: BuildReasonMessage
): Array<WrappedFieldsLatest<BaseFieldsLatest>> => {
const wrappedDocs = events.map((event): WrappedFieldsLatest<BaseFieldsLatest> => {
const id = generateId(
event._index,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
event._id!,
String(event._version),
`${sharedParams.spaceId}:${sharedParams.completeRule.alertId}`
);
const baseAlert = transformHitToAlert({
sharedParams,
doc: event as SimpleHit,
applyOverrides: true,
buildReasonMessage,
alertUuid: id,
});
return {
_id: id,
_index: '',
_source: {
...baseAlert,
},
};
});
return wrappedDocs.filter(
(doc) =>
!doc._source['kibana.alert.ancestors'].some(
(ancestor) => ancestor.rule === sharedParams.completeRule.alertId
)
);
};

View file

@ -1,89 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { estypes } from '@elastic/elasticsearch';
import type { ConfigType } from '../../../../config';
import type { SignalSource, SimpleHit } from '../types';
import type { CompleteRule, RuleParams } from '../../rule_schema';
import { generateId } from '../utils/utils';
import { transformHitToAlert } from './utils/transform_hit_to_alert';
import type { BuildReasonMessage } from '../utils/reason_formatters';
import type {
BaseFieldsLatest,
WrappedFieldsLatest,
} from '../../../../../common/api/detection_engine/model/alerts';
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
export const wrapHitsFactory =
({
completeRule,
ignoreFields,
ignoreFieldsRegexes,
mergeStrategy,
spaceId,
indicesToQuery,
alertTimestampOverride,
publicBaseUrl,
ruleExecutionLogger,
intendedTimestamp,
}: {
completeRule: CompleteRule<RuleParams>;
ignoreFields: Record<string, boolean>;
ignoreFieldsRegexes: string[];
mergeStrategy: ConfigType['alertMergeStrategy'];
spaceId: string | null | undefined;
indicesToQuery: string[];
alertTimestampOverride: Date | undefined;
publicBaseUrl: string | undefined;
ruleExecutionLogger: IRuleExecutionLogForExecutors;
intendedTimestamp: Date | undefined;
}) =>
(
events: Array<estypes.SearchHit<SignalSource>>,
buildReasonMessage: BuildReasonMessage
): Array<WrappedFieldsLatest<BaseFieldsLatest>> => {
const wrappedDocs = events.map((event): WrappedFieldsLatest<BaseFieldsLatest> => {
const id = generateId(
event._index,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
event._id!,
String(event._version),
`${spaceId}:${completeRule.alertId}`
);
const baseAlert = transformHitToAlert({
spaceId,
completeRule,
doc: event as SimpleHit,
mergeStrategy,
ignoreFields,
ignoreFieldsRegexes,
applyOverrides: true,
buildReasonMessage,
indicesToQuery,
alertTimestampOverride,
ruleExecutionLogger,
alertUuid: id,
publicBaseUrl,
intendedTimestamp,
});
return {
_id: id,
_index: '',
_source: {
...baseAlert,
},
};
});
return wrappedDocs.filter(
(doc) =>
!doc._source['kibana.alert.ancestors'].some(
(ancestor) => ancestor.rule === completeRule.alertId
)
);
};

View file

@ -13,26 +13,14 @@ import type {
AlertInstanceState,
RuleExecutorServices,
} from '@kbn/alerting-plugin/server';
import type { GenericBulkCreateResponse } from '../factories';
import { wrapHits, type GenericBulkCreateResponse } from '../factories';
import type { Anomaly } from '../../../machine_learning';
import type { BulkCreate, WrapHits } from '../types';
import type { CompleteRule, MachineLearningRuleParams } from '../../rule_schema';
import type { SecuritySharedParams } from '../types';
import type { MachineLearningRuleParams } from '../../rule_schema';
import { buildReasonMessageForMlAlert } from '../utils/reason_formatters';
import type { BaseFieldsLatest } from '../../../../../common/api/detection_engine/model/alerts';
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
import { createEnrichEventsFunction } from '../utils/enrichments';
interface BulkCreateMlSignalsParams {
anomalyHits: Array<estypes.SearchHit<Anomaly>>;
completeRule: CompleteRule<MachineLearningRuleParams>;
services: RuleExecutorServices<AlertInstanceState, AlertInstanceContext, 'default'>;
ruleExecutionLogger: IRuleExecutionLogForExecutors;
id: string;
signalsIndex: string;
bulkCreate: BulkCreate;
wrapHits: WrapHits;
}
interface EcsAnomaly extends Anomaly {
'@timestamp': string;
}
@ -75,19 +63,24 @@ const transformAnomalyResultsToEcs = (
}));
};
export const bulkCreateMlSignals = async (
params: BulkCreateMlSignalsParams
): Promise<GenericBulkCreateResponse<BaseFieldsLatest>> => {
const anomalyResults = params.anomalyHits;
const ecsResults = transformAnomalyResultsToEcs(anomalyResults);
export const bulkCreateMlSignals = async ({
sharedParams,
anomalyHits,
services,
}: {
sharedParams: SecuritySharedParams<MachineLearningRuleParams>;
anomalyHits: Array<estypes.SearchHit<Anomaly>>;
services: RuleExecutorServices<AlertInstanceState, AlertInstanceContext, 'default'>;
}): Promise<GenericBulkCreateResponse<BaseFieldsLatest>> => {
const ecsResults = transformAnomalyResultsToEcs(anomalyHits);
const wrappedDocs = params.wrapHits(ecsResults, buildReasonMessageForMlAlert);
return params.bulkCreate(
const wrappedDocs = wrapHits(sharedParams, ecsResults, buildReasonMessageForMlAlert);
return sharedParams.bulkCreate(
wrappedDocs,
undefined,
createEnrichEventsFunction({
services: params.services,
logger: params.ruleExecutionLogger,
services,
logger: sharedParams.ruleExecutionLogger,
})
);
};

View file

@ -62,8 +62,6 @@ export const mlExecutor = async ({
exceptionFilter,
listClient,
unprocessedExceptions,
bulkCreate,
wrapHits,
} = sharedParams;
const result = createSearchAfterReturnType();
const ruleParams = completeRule.ruleParams;
@ -166,14 +164,9 @@ export const mlExecutor = async ({
});
} else {
const createResult = await bulkCreateMlSignals({
sharedParams,
anomalyHits: filteredAnomalyHits,
completeRule,
services,
ruleExecutionLogger,
id: completeRule.alertId,
signalsIndex: ruleParams.outputIndex,
bulkCreate,
wrapHits,
});
addToSearchAfterReturn({ current: result, next: createResult });
}

View file

@ -5,10 +5,10 @@
* 2.0.
*/
import type { SuppressionFieldsLatest } from '@kbn/rule-registry-plugin/common/schemas';
import type {
SearchAfterAndBulkCreateParams,
SearchAfterAndBulkCreateReturnType,
SecuritySharedParams,
WrapSuppressedHits,
} from '../types';
@ -20,29 +20,21 @@ import { executeBulkCreateAlerts } from '../utils/bulk_create_suppressed_alerts_
import type {
BaseFieldsLatest,
WrappedFieldsLatest,
NewTermsFieldsLatest,
} from '../../../../../common/api/detection_engine/model/alerts';
import { partitionMissingFieldsEvents } from '../utils/partition_missing_fields_events';
import type { EventsAndTerms } from './types';
import type { ExperimentalFeatures } from '../../../../../common';
import { wrapNewTermsAlerts } from './wrap_new_terms_alerts';
import { wrapSuppressedNewTermsAlerts } from './wrap_suppressed_new_terms_alerts';
import type { NewTermsRuleParams } from '../../rule_schema';
interface SearchAfterAndBulkCreateSuppressedAlertsParams extends SearchAfterAndBulkCreateParams {
wrapSuppressedHits: WrapSuppressedHits;
alertSuppression?: AlertSuppressionCamel;
}
export interface BulkCreateSuppressedAlertsParams
extends Pick<
SearchAfterAndBulkCreateSuppressedAlertsParams,
'sharedParams' | 'services' | 'alertSuppression'
> {
wrapHits: (
events: EventsAndTerms[]
) => Array<WrappedFieldsLatest<BaseFieldsLatest & NewTermsFieldsLatest>>;
wrapSuppressedHits: (
events: EventsAndTerms[]
) => Array<
WrappedFieldsLatest<BaseFieldsLatest & SuppressionFieldsLatest & NewTermsFieldsLatest>
>;
extends Pick<SearchAfterAndBulkCreateSuppressedAlertsParams, 'services' | 'alertSuppression'> {
sharedParams: SecuritySharedParams<NewTermsRuleParams>;
eventsAndTerms: EventsAndTerms[];
toReturn: SearchAfterAndBulkCreateReturnType;
experimentalFeatures: ExperimentalFeatures;
@ -56,8 +48,6 @@ export interface BulkCreateSuppressedAlertsParams
export const bulkCreateSuppressedNewTermsAlertsInMemory = async ({
sharedParams,
eventsAndTerms,
wrapHits,
wrapSuppressedHits,
toReturn,
services,
alertSuppression,
@ -77,12 +67,18 @@ export const bulkCreateSuppressedNewTermsAlertsInMemory = async ({
['event', 'fields']
);
unsuppressibleWrappedDocs = wrapHits(partitionedEvents[1]);
unsuppressibleWrappedDocs = wrapNewTermsAlerts({
sharedParams,
eventsAndTerms: partitionedEvents[1],
});
suppressibleEvents = partitionedEvents[0];
}
const suppressibleWrappedDocs = wrapSuppressedHits(suppressibleEvents);
const suppressibleWrappedDocs = wrapSuppressedNewTermsAlerts({
sharedParams,
eventsAndTerms: suppressibleEvents,
});
return executeBulkCreateAlerts({
sharedParams,

View file

@ -16,7 +16,6 @@ import type { CreateRuleOptions, SecurityAlertType } from '../types';
import { singleSearchAfter } from '../utils/single_search_after';
import { getFilter } from '../utils/get_filter';
import { wrapNewTermsAlerts } from './wrap_new_terms_alerts';
import { wrapSuppressedNewTermsAlerts } from './wrap_suppressed_new_terms_alerts';
import { bulkCreateSuppressedNewTermsAlertsInMemory } from './bulk_create_suppressed_alerts_in_memory';
import type { EventsAndTerms } from './types';
import type {
@ -98,14 +97,13 @@ export const createNewTermsAlertType = (
producer: SERVER_APP_ID,
solution: 'security',
async executor(execOptions) {
const { sharedParams, services, params, spaceId, state } = execOptions;
const { sharedParams, services, params, state } = execOptions;
const {
ruleExecutionLogger,
bulkCreate,
completeRule,
tuple,
mergeStrategy,
inputIndex,
runtimeMappings,
primaryTimestamp,
@ -113,9 +111,6 @@ export const createNewTermsAlertType = (
aggregatableTimestampField,
exceptionFilter,
unprocessedExceptions,
alertTimestampOverride,
publicBaseUrl,
intendedTimestamp,
} = sharedParams;
const isLoggedRequestsEnabled = Boolean(state?.isLoggedRequestsEnabled);
@ -222,34 +217,6 @@ export const createNewTermsAlertType = (
const bucketsForField = searchResultWithAggs.aggregations.new_terms.buckets;
const createAlertsHook: CreateAlertsHook = async (aggResult) => {
const wrapHits = (eventsAndTerms: EventsAndTerms[]) =>
wrapNewTermsAlerts({
eventsAndTerms,
spaceId,
completeRule,
mergeStrategy,
indicesToQuery: inputIndex,
alertTimestampOverride,
ruleExecutionLogger,
publicBaseUrl,
intendedTimestamp,
});
const wrapSuppressedHits = (eventsAndTerms: EventsAndTerms[]) =>
wrapSuppressedNewTermsAlerts({
eventsAndTerms,
spaceId,
completeRule,
mergeStrategy,
indicesToQuery: inputIndex,
alertTimestampOverride,
ruleExecutionLogger,
publicBaseUrl,
primaryTimestamp,
secondaryTimestamp,
intendedTimestamp,
});
const eventsAndTerms: EventsAndTerms[] = (
aggResult?.aggregations?.new_terms.buckets ?? []
).map((bucket) => {
@ -286,14 +253,15 @@ export const createNewTermsAlertType = (
sharedParams,
eventsAndTerms: eventAndTermsChunk,
toReturn: result,
wrapHits,
services,
alertSuppression: params.alertSuppression,
wrapSuppressedHits,
experimentalFeatures,
});
} else {
const wrappedAlerts = wrapHits(eventAndTermsChunk);
const wrappedAlerts = wrapNewTermsAlerts({
sharedParams,
eventsAndTerms: eventAndTermsChunk,
});
bulkCreateResult = await bulkCreate(
wrappedAlerts,

View file

@ -7,29 +7,26 @@
import { ALERT_URL, ALERT_UUID } from '@kbn/rule-data-utils';
import { ALERT_NEW_TERMS } from '../../../../../common/field_maps/field_names';
import { getCompleteRuleMock, getNewTermsRuleParams } from '../../rule_schema/mocks';
import { ruleExecutionLogMock } from '../../rule_monitoring/mocks';
import { getNewTermsRuleParams } from '../../rule_schema/mocks';
import { sampleDocNoSortIdWithTimestamp } from '../__mocks__/es_results';
import { wrapNewTermsAlerts } from './wrap_new_terms_alerts';
const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create();
import { getSharedParamsMock } from '../__mocks__/shared_params';
const docId = 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71';
const publicBaseUrl = 'http://somekibanabaseurl.com';
const sharedParams = getSharedParamsMock({
ruleParams: getNewTermsRuleParams(),
rewrites: {
publicBaseUrl,
inputIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
},
});
describe('wrapNewTermsAlerts', () => {
test('should create an alert with the correct _id from a document', () => {
const doc = sampleDocNoSortIdWithTimestamp(docId);
const completeRule = getCompleteRuleMock(getNewTermsRuleParams());
const alerts = wrapNewTermsAlerts({
sharedParams,
eventsAndTerms: [{ event: doc, newTerms: ['127.0.0.1'] }],
spaceId: 'default',
mergeStrategy: 'missingFields',
completeRule,
indicesToQuery: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl,
intendedTimestamp: undefined,
});
expect(alerts[0]._id).toEqual('a36d9fe6fe4b2f65058fb1a487733275f811af58');
@ -42,17 +39,17 @@ describe('wrapNewTermsAlerts', () => {
test('should create an alert with a different _id if the space is different', () => {
const doc = sampleDocNoSortIdWithTimestamp(docId);
const completeRule = getCompleteRuleMock(getNewTermsRuleParams());
const newSharedParams = getSharedParamsMock({
ruleParams: getNewTermsRuleParams(),
rewrites: {
publicBaseUrl,
inputIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
spaceId: 'otherSpace',
},
});
const alerts = wrapNewTermsAlerts({
sharedParams: newSharedParams,
eventsAndTerms: [{ event: doc, newTerms: ['127.0.0.1'] }],
spaceId: 'otherSpace',
mergeStrategy: 'missingFields',
completeRule,
indicesToQuery: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl,
intendedTimestamp: undefined,
});
expect(alerts[0]._id).toEqual('f7877a31b1cc83373dbc9ba5939ebfab1db66545');
@ -65,17 +62,17 @@ describe('wrapNewTermsAlerts', () => {
test('should create an alert with a different _id if the newTerms array is different', () => {
const doc = sampleDocNoSortIdWithTimestamp(docId);
const completeRule = getCompleteRuleMock(getNewTermsRuleParams());
const newSharedParams = getSharedParamsMock({
ruleParams: getNewTermsRuleParams(),
rewrites: {
publicBaseUrl,
inputIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
spaceId: 'otherSpace',
},
});
const alerts = wrapNewTermsAlerts({
sharedParams: newSharedParams,
eventsAndTerms: [{ event: doc, newTerms: ['127.0.0.2'] }],
spaceId: 'otherSpace',
mergeStrategy: 'missingFields',
completeRule,
indicesToQuery: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl,
intendedTimestamp: undefined,
});
expect(alerts[0]._id).toEqual('75e5a507a4bc48bcd983820c7fd2d9621ff4e2ea');
@ -88,17 +85,17 @@ describe('wrapNewTermsAlerts', () => {
test('should create an alert with a different _id if the newTerms array contains multiple terms', () => {
const doc = sampleDocNoSortIdWithTimestamp(docId);
const completeRule = getCompleteRuleMock(getNewTermsRuleParams());
const newSharedParams = getSharedParamsMock({
ruleParams: getNewTermsRuleParams(),
rewrites: {
publicBaseUrl,
inputIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
spaceId: 'otherSpace',
},
});
const alerts = wrapNewTermsAlerts({
sharedParams: newSharedParams,
eventsAndTerms: [{ event: doc, newTerms: ['127.0.0.1', '127.0.0.2'] }],
spaceId: 'otherSpace',
mergeStrategy: 'missingFields',
completeRule,
indicesToQuery: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl,
intendedTimestamp: undefined,
});
expect(alerts[0]._id).toEqual('86a216cfa4884767d9bb26d2b8db911cb4aa85ce');

View file

@ -13,11 +13,8 @@ import type {
WrappedFieldsLatest,
} from '../../../../../common/api/detection_engine/model/alerts';
import { ALERT_NEW_TERMS } from '../../../../../common/field_maps/field_names';
import type { ConfigType } from '../../../../config';
import type { CompleteRule, RuleParams } from '../../rule_schema';
import { buildReasonMessageForNewTermsAlert } from '../utils/reason_formatters';
import type { SignalSource } from '../types';
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
import type { SecuritySharedParams, SignalSource } from '../types';
import { transformHitToAlert } from '../factories/utils/transform_hit_to_alert';
export interface EventsAndTerms {
@ -26,49 +23,26 @@ export interface EventsAndTerms {
}
export const wrapNewTermsAlerts = ({
sharedParams,
eventsAndTerms,
spaceId,
completeRule,
mergeStrategy,
indicesToQuery,
alertTimestampOverride,
ruleExecutionLogger,
publicBaseUrl,
intendedTimestamp,
}: {
sharedParams: SecuritySharedParams;
eventsAndTerms: EventsAndTerms[];
spaceId: string | null | undefined;
completeRule: CompleteRule<RuleParams>;
mergeStrategy: ConfigType['alertMergeStrategy'];
indicesToQuery: string[];
alertTimestampOverride: Date | undefined;
ruleExecutionLogger: IRuleExecutionLogForExecutors;
publicBaseUrl: string | undefined;
intendedTimestamp: Date | undefined;
}): Array<WrappedFieldsLatest<NewTermsFieldsLatest>> => {
return eventsAndTerms.map((eventAndTerms) => {
const id = objectHash([
eventAndTerms.event._index,
eventAndTerms.event._id,
String(eventAndTerms.event._version),
`${spaceId}:${completeRule.alertId}`,
`${sharedParams.spaceId}:${sharedParams.completeRule.alertId}`,
eventAndTerms.newTerms,
]);
const baseAlert: BaseFieldsLatest = transformHitToAlert({
spaceId,
completeRule,
sharedParams,
doc: eventAndTerms.event,
mergeStrategy,
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage: buildReasonMessageForNewTermsAlert,
indicesToQuery,
alertTimestampOverride,
ruleExecutionLogger,
alertUuid: id,
publicBaseUrl,
intendedTimestamp,
});
return {

View file

@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { cloneDeep } from 'lodash';
import {
ALERT_URL,
@ -17,11 +16,9 @@ import {
} from '@kbn/rule-data-utils';
import { ALERT_NEW_TERMS } from '../../../../../common/field_maps/field_names';
import { getCompleteRuleMock, getNewTermsRuleParams } from '../../rule_schema/mocks';
import { ruleExecutionLogMock } from '../../rule_monitoring/mocks';
import { sampleDocNoSortIdWithTimestamp } from '../__mocks__/es_results';
import { wrapSuppressedNewTermsAlerts } from './wrap_suppressed_new_terms_alerts';
const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create();
import { getSharedParamsMock } from '../__mocks__/shared_params';
const docId = 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71';
const publicBaseUrl = 'http://somekibanabaseurl.com';
@ -33,20 +30,20 @@ const alertSuppression = {
const completeRule = getCompleteRuleMock(getNewTermsRuleParams());
completeRule.ruleParams.alertSuppression = alertSuppression;
const sharedParams = getSharedParamsMock({
ruleParams: getNewTermsRuleParams({ alertSuppression }),
rewrites: {
publicBaseUrl,
inputIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
},
});
describe('wrapSuppressedNewTermsAlerts', () => {
test('should create an alert with the correct _id from a document and suppression fields', () => {
const doc = sampleDocNoSortIdWithTimestamp(docId);
const alerts = wrapSuppressedNewTermsAlerts({
sharedParams,
eventsAndTerms: [{ event: doc, newTerms: ['127.0.0.1'] }],
spaceId: 'default',
mergeStrategy: 'missingFields',
completeRule,
indicesToQuery: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl,
primaryTimestamp: '@timestamp',
intendedTimestamp: undefined,
});
expect(alerts[0]._id).toEqual('a36d9fe6fe4b2f65058fb1a487733275f811af58');
@ -67,22 +64,20 @@ describe('wrapSuppressedNewTermsAlerts', () => {
});
test('should create an alert with a different _id if suppression field is different', () => {
const completeRuleCloned = cloneDeep(completeRule);
completeRuleCloned.ruleParams.alertSuppression = {
groupBy: ['someKey'],
};
const doc = sampleDocNoSortIdWithTimestamp(docId);
const alerts = wrapSuppressedNewTermsAlerts({
sharedParams: getSharedParamsMock({
ruleParams: getNewTermsRuleParams({
alertSuppression: {
groupBy: ['someKey'],
},
}),
rewrites: {
publicBaseUrl,
inputIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
},
}),
eventsAndTerms: [{ event: doc, newTerms: ['127.0.0.1'] }],
spaceId: 'default',
mergeStrategy: 'missingFields',
completeRule: completeRuleCloned,
indicesToQuery: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl,
primaryTimestamp: '@timestamp',
intendedTimestamp: undefined,
});
expect(alerts[0]._id).toEqual('a36d9fe6fe4b2f65058fb1a487733275f811af58');
@ -102,16 +97,15 @@ describe('wrapSuppressedNewTermsAlerts', () => {
test('should create an alert with a different _id if the space is different', () => {
const doc = sampleDocNoSortIdWithTimestamp(docId);
const alerts = wrapSuppressedNewTermsAlerts({
sharedParams: getSharedParamsMock({
ruleParams: getNewTermsRuleParams({ alertSuppression }),
rewrites: {
publicBaseUrl,
inputIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
spaceId: 'otherSpace',
},
}),
eventsAndTerms: [{ event: doc, newTerms: ['127.0.0.1'] }],
spaceId: 'otherSpace',
mergeStrategy: 'missingFields',
completeRule,
indicesToQuery: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl,
primaryTimestamp: '@timestamp',
intendedTimestamp: undefined,
});
expect(alerts[0]._id).toEqual('f7877a31b1cc83373dbc9ba5939ebfab1db66545');
@ -124,16 +118,15 @@ describe('wrapSuppressedNewTermsAlerts', () => {
test('should create an alert with a different _id if the newTerms array is different', () => {
const doc = sampleDocNoSortIdWithTimestamp(docId);
const alerts = wrapSuppressedNewTermsAlerts({
sharedParams: getSharedParamsMock({
ruleParams: getNewTermsRuleParams({ alertSuppression }),
rewrites: {
publicBaseUrl,
inputIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
spaceId: 'otherSpace',
},
}),
eventsAndTerms: [{ event: doc, newTerms: ['127.0.0.2'] }],
spaceId: 'otherSpace',
mergeStrategy: 'missingFields',
completeRule,
indicesToQuery: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl,
primaryTimestamp: '@timestamp',
intendedTimestamp: undefined,
});
expect(alerts[0]._id).toEqual('75e5a507a4bc48bcd983820c7fd2d9621ff4e2ea');

View file

@ -15,13 +15,11 @@ import type {
WrappedFieldsLatest,
} from '../../../../../common/api/detection_engine/model/alerts';
import { ALERT_NEW_TERMS } from '../../../../../common/field_maps/field_names';
import type { ConfigType } from '../../../../config';
import type { CompleteRule, NewTermsRuleParams } from '../../rule_schema';
import { buildReasonMessageForNewTermsAlert } from '../utils/reason_formatters';
import { getSuppressionAlertFields, getSuppressionTerms } from '../utils';
import type { SignalSource } from '../types';
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
import type { SecuritySharedParams, SignalSource } from '../types';
import { transformHitToAlert } from '../factories/utils/transform_hit_to_alert';
import type { NewTermsRuleParams } from '../../rule_schema';
export interface EventsAndTerms {
event: estypes.SearchHit<SignalSource>;
@ -29,32 +27,15 @@ export interface EventsAndTerms {
}
export const wrapSuppressedNewTermsAlerts = ({
sharedParams,
eventsAndTerms,
spaceId,
completeRule,
mergeStrategy,
indicesToQuery,
alertTimestampOverride,
ruleExecutionLogger,
publicBaseUrl,
primaryTimestamp,
secondaryTimestamp,
intendedTimestamp,
}: {
sharedParams: SecuritySharedParams<NewTermsRuleParams>;
eventsAndTerms: EventsAndTerms[];
spaceId: string | null | undefined;
completeRule: CompleteRule<NewTermsRuleParams>;
mergeStrategy: ConfigType['alertMergeStrategy'];
indicesToQuery: string[];
alertTimestampOverride: Date | undefined;
ruleExecutionLogger: IRuleExecutionLogForExecutors;
publicBaseUrl: string | undefined;
primaryTimestamp: string;
secondaryTimestamp?: string;
intendedTimestamp: Date | undefined;
}): Array<WrappedFieldsLatest<NewTermsFieldsLatest & SuppressionFieldsLatest>> => {
return eventsAndTerms.map((eventAndTerms) => {
const event = eventAndTerms.event;
const { completeRule, spaceId } = sharedParams;
const suppressionTerms = getSuppressionTerms({
alertSuppression: completeRule?.ruleParams?.alertSuppression,
@ -72,20 +53,11 @@ export const wrapSuppressedNewTermsAlerts = ({
]);
const baseAlert: BaseFieldsLatest = transformHitToAlert({
spaceId,
completeRule,
sharedParams,
doc: event,
mergeStrategy,
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage: buildReasonMessageForNewTermsAlert,
indicesToQuery,
alertTimestampOverride,
ruleExecutionLogger,
alertUuid: id,
publicBaseUrl,
intendedTimestamp,
});
return {
@ -95,8 +67,8 @@ export const wrapSuppressedNewTermsAlerts = ({
...baseAlert,
[ALERT_NEW_TERMS]: eventAndTerms.newTerms,
...getSuppressionAlertFields({
primaryTimestamp,
secondaryTimestamp,
primaryTimestamp: sharedParams.primaryTimestamp,
secondaryTimestamp: sharedParams.secondaryTimestamp,
fields: event.fields,
suppressionTerms,
fallbackTimestamp: baseAlert[TIMESTAMP],

View file

@ -267,16 +267,9 @@ export const groupAndBulkCreate = async ({
}));
const wrappedAlerts = wrapSuppressedAlerts({
sharedParams,
suppressionBuckets,
spaceId: sharedParams.spaceId,
completeRule: sharedParams.completeRule,
mergeStrategy: sharedParams.mergeStrategy,
indicesToQuery: sharedParams.inputIndex,
publicBaseUrl: sharedParams.publicBaseUrl,
buildReasonMessage,
alertTimestampOverride: sharedParams.alertTimestampOverride,
ruleExecutionLogger: sharedParams.ruleExecutionLogger,
intendedTimestamp: sharedParams.intendedTimestamp,
});
const suppressionDuration = sharedParams.completeRule.ruleParams.alertSuppression?.duration;

View file

@ -19,10 +19,7 @@ import type {
BaseFieldsLatest,
WrappedFieldsLatest,
} from '../../../../../../common/api/detection_engine/model/alerts';
import type { ConfigType } from '../../../../../config';
import type { CompleteRule, RuleParams } from '../../../rule_schema';
import type { IRuleExecutionLogForExecutors } from '../../../rule_monitoring';
import type { SignalSource } from '../../types';
import type { SecuritySharedParams, SignalSource } from '../../types';
import { transformHitToAlert } from '../../factories/utils/transform_hit_to_alert';
import type { BuildReasonMessage } from '../../utils/reason_formatters';
@ -47,28 +44,15 @@ export const createSuppressedAlertInstanceId = ({
};
export const wrapSuppressedAlerts = ({
sharedParams,
suppressionBuckets,
spaceId,
completeRule,
mergeStrategy,
indicesToQuery,
buildReasonMessage,
alertTimestampOverride,
ruleExecutionLogger,
publicBaseUrl,
intendedTimestamp,
}: {
sharedParams: SecuritySharedParams;
suppressionBuckets: SuppressionBucket[];
spaceId: string;
completeRule: CompleteRule<RuleParams>;
mergeStrategy: ConfigType['alertMergeStrategy'];
indicesToQuery: string[];
buildReasonMessage: BuildReasonMessage;
alertTimestampOverride: Date | undefined;
ruleExecutionLogger: IRuleExecutionLogForExecutors;
publicBaseUrl: string | undefined;
intendedTimestamp: Date | undefined;
}): Array<WrappedFieldsLatest<BaseFieldsLatest & SuppressionFieldsLatest>> => {
const { completeRule, spaceId } = sharedParams;
return suppressionBuckets.map((bucket) => {
const id = objectHash([
bucket.event._index,
@ -85,20 +69,11 @@ export const wrapSuppressedAlerts = ({
spaceId,
});
const baseAlert: BaseFieldsLatest = transformHitToAlert({
spaceId,
completeRule,
sharedParams,
doc: bucket.event,
mergeStrategy,
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage,
indicesToQuery,
alertTimestampOverride,
ruleExecutionLogger,
alertUuid: id,
publicBaseUrl,
intendedTimestamp,
});
return {

View file

@ -29,8 +29,6 @@ interface BulkCreateSuppressedThresholdAlertsParams {
buckets: ThresholdBucket[];
services: RuleExecutorServices<AlertInstanceState, AlertInstanceContext, 'default'>;
startedAt: Date;
from: Date;
to: Date;
experimentalFeatures: ExperimentalFeatures;
}
@ -44,24 +42,12 @@ export const bulkCreateSuppressedThresholdAlerts = async ({
buckets,
services,
startedAt,
from,
to,
experimentalFeatures,
}: BulkCreateSuppressedThresholdAlertsParams): Promise<{
bulkCreateResult: GenericBulkCreateResponse<BaseFieldsLatest & SuppressionFieldsLatest>;
unsuppressedAlerts: Array<SearchHit<unknown>>;
}> => {
const {
completeRule,
mergeStrategy,
inputIndex,
alertTimestampOverride,
ruleExecutionLogger,
publicBaseUrl,
intendedTimestamp,
} = sharedParams;
const ruleParams = completeRule.ruleParams;
const suppressionDuration = completeRule.ruleParams.alertSuppression?.duration;
const suppressionDuration = sharedParams.completeRule.ruleParams.alertSuppression?.duration;
if (!suppressionDuration) {
throw Error('Suppression duration can not be empty');
}
@ -69,22 +55,10 @@ export const bulkCreateSuppressedThresholdAlerts = async ({
const suppressionWindow = `now-${suppressionDuration.value}${suppressionDuration.unit}`;
const wrappedAlerts = wrapSuppressedThresholdALerts({
sharedParams,
buckets,
spaceId: sharedParams.spaceId,
completeRule,
mergeStrategy,
indicesToQuery: inputIndex,
buildReasonMessage: buildReasonMessageForThresholdAlert,
alertTimestampOverride,
ruleExecutionLogger,
publicBaseUrl,
inputIndex: inputIndex.join(','),
startedAt,
from,
to,
suppressionWindow,
threshold: ruleParams.threshold,
intendedTimestamp,
});
const bulkCreateResult = await bulkCreateWithSuppression({

View file

@ -16,26 +16,18 @@ import type { ThresholdNormalized } from '../../../../../common/api/detection_en
import type { GenericBulkCreateResponse } from '../factories/bulk_create_factory';
import { calculateThresholdSignalUuid } from './utils';
import { buildReasonMessageForThresholdAlert } from '../utils/reason_formatters';
import type { ThresholdSignalHistory, ThresholdBucket } from './types';
import type { BulkCreate, WrapHits } from '../types';
import type { CompleteRule, ThresholdRuleParams } from '../../rule_schema';
import type { ThresholdBucket } from './types';
import type { SecuritySharedParams } from '../types';
import type { ThresholdRuleParams } from '../../rule_schema';
import type { BaseFieldsLatest } from '../../../../../common/api/detection_engine/model/alerts';
import { createEnrichEventsFunction } from '../utils/enrichments';
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
import { wrapHits } from '../factories';
interface BulkCreateThresholdSignalsParams {
sharedParams: SecuritySharedParams<ThresholdRuleParams>;
buckets: ThresholdBucket[];
completeRule: CompleteRule<ThresholdRuleParams>;
services: RuleExecutorServices<AlertInstanceState, AlertInstanceContext, 'default'>;
inputIndexPattern: string[];
filter: unknown;
signalsIndex: string;
startedAt: Date;
from: Date;
signalHistory: ThresholdSignalHistory;
bulkCreate: BulkCreate;
wrapHits: WrapHits;
ruleExecutionLogger: IRuleExecutionLogForExecutors;
}
export const transformBucketIntoHit = (
@ -90,25 +82,28 @@ export const getTransformedHits = (
return transformBucketIntoHit(bucket, inputIndex, startedAt, from, threshold, ruleId);
});
export const bulkCreateThresholdSignals = async (
params: BulkCreateThresholdSignalsParams
): Promise<GenericBulkCreateResponse<BaseFieldsLatest>> => {
const ruleParams = params.completeRule.ruleParams;
export const bulkCreateThresholdSignals = async ({
sharedParams,
buckets,
services,
startedAt,
}: BulkCreateThresholdSignalsParams): Promise<GenericBulkCreateResponse<BaseFieldsLatest>> => {
const ruleParams = sharedParams.completeRule.ruleParams;
const ecsResults = getTransformedHits(
params.buckets,
params.inputIndexPattern.join(','),
params.startedAt,
params.from,
buckets,
sharedParams.inputIndex.join(','),
startedAt,
sharedParams.tuple.from.toDate(),
ruleParams.threshold,
ruleParams.ruleId
);
return params.bulkCreate(
params.wrapHits(ecsResults, buildReasonMessageForThresholdAlert),
return sharedParams.bulkCreate(
wrapHits(sharedParams, ecsResults, buildReasonMessageForThresholdAlert),
undefined,
createEnrichEventsFunction({
services: params.services,
logger: params.ruleExecutionLogger,
services,
logger: sharedParams.ruleExecutionLogger,
})
);
};

View file

@ -69,8 +69,6 @@ export const thresholdExecutor = async ({
primaryTimestamp,
secondaryTimestamp,
runtimeMappings,
bulkCreate,
wrapHits,
} = sharedParams;
const result = createSearchAfterReturnType();
const ruleParams = completeRule.ruleParams;
@ -146,8 +144,6 @@ export const thresholdExecutor = async ({
buckets,
services,
startedAt,
from: tuple.from.toDate(),
to: tuple.to.toDate(),
experimentalFeatures,
});
const createResult = suppressedResults.bulkCreateResult;
@ -161,18 +157,10 @@ export const thresholdExecutor = async ({
});
} else {
const createResult = await bulkCreateThresholdSignals({
sharedParams,
buckets,
completeRule,
filter: esFilter,
services,
inputIndexPattern: inputIndex,
signalsIndex: ruleParams.outputIndex,
startedAt,
from: tuple.from.toDate(),
signalHistory: validSignalHistory,
bulkCreate,
wrapHits,
ruleExecutionLogger,
});
newSignalHistory = buildThresholdSignalHistory({

View file

@ -22,15 +22,13 @@ import type {
BaseFieldsLatest,
WrappedFieldsLatest,
} from '../../../../../common/api/detection_engine/model/alerts';
import type { ConfigType } from '../../../../config';
import type { CompleteRule, ThresholdRuleParams } from '../../rule_schema';
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
import { transformHitToAlert } from '../factories/utils/transform_hit_to_alert';
import type { ThresholdBucket } from './types';
import type { BuildReasonMessage } from '../utils/reason_formatters';
import { transformBucketIntoHit } from './bulk_create_threshold_signals';
import type { ThresholdNormalized } from '../../../../../common/api/detection_engine/model/rule_schema';
import type { SecuritySharedParams } from '../types';
import type { ThresholdRuleParams } from '../../rule_schema';
/**
* wraps suppressed threshold alerts
@ -39,46 +37,24 @@ import type { ThresholdNormalized } from '../../../../../common/api/detection_en
* populates alert's suppression fields
*/
export const wrapSuppressedThresholdALerts = ({
sharedParams,
buckets,
spaceId,
completeRule,
mergeStrategy,
indicesToQuery,
buildReasonMessage,
alertTimestampOverride,
ruleExecutionLogger,
publicBaseUrl,
inputIndex,
startedAt,
from,
to,
threshold,
intendedTimestamp,
}: {
sharedParams: SecuritySharedParams<ThresholdRuleParams>;
buckets: ThresholdBucket[];
spaceId: string;
completeRule: CompleteRule<ThresholdRuleParams>;
mergeStrategy: ConfigType['alertMergeStrategy'];
indicesToQuery: string[];
buildReasonMessage: BuildReasonMessage;
alertTimestampOverride: Date | undefined;
ruleExecutionLogger: IRuleExecutionLogForExecutors;
publicBaseUrl: string | undefined;
inputIndex: string;
startedAt: Date;
from: Date;
to: Date;
suppressionWindow: string;
threshold: ThresholdNormalized;
intendedTimestamp: Date | undefined;
}): Array<WrappedFieldsLatest<BaseFieldsLatest & SuppressionFieldsLatest>> => {
const { completeRule, spaceId } = sharedParams;
return buckets.map((bucket) => {
const hit = transformBucketIntoHit(
bucket,
inputIndex,
sharedParams.inputIndex.join(','),
startedAt,
from,
threshold,
sharedParams.tuple.from.toDate(),
completeRule.ruleParams.threshold,
completeRule.ruleParams.ruleId
);
@ -89,20 +65,11 @@ export const wrapSuppressedThresholdALerts = ({
const instanceId = objectHash([suppressedValues, completeRule.alertId, spaceId]);
const baseAlert: BaseFieldsLatest = transformHitToAlert({
spaceId,
completeRule,
sharedParams,
doc: hit,
mergeStrategy,
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage,
indicesToQuery,
alertTimestampOverride,
ruleExecutionLogger,
alertUuid: id,
publicBaseUrl,
intendedTimestamp,
});
return {

View file

@ -89,7 +89,6 @@ export interface SecuritySharedParams<TParams extends RuleParams = RuleParams> {
listClient: ListClient;
searchAfterSize: number;
bulkCreate: BulkCreate;
wrapHits: WrapHits;
ruleDataClient: IRuleDataClient;
inputIndex: string[];
runtimeMappings: estypes.MappingRuntimeFields | undefined;
@ -106,6 +105,8 @@ export interface SecuritySharedParams<TParams extends RuleParams = RuleParams> {
experimentalFeatures?: ExperimentalFeatures;
intendedTimestamp: Date | undefined;
spaceId: string;
ignoreFields: Record<string, boolean>;
ignoreFieldsRegexes: string[];
}
export type SecurityAlertType<
@ -340,11 +341,6 @@ export type BulkCreate = <T extends BaseFieldsLatest>(
export type SimpleHit = BaseHit<{ '@timestamp'?: string }>;
export type WrapHits = (
hits: Array<estypes.SearchHit<SignalSource>>,
buildReasonMessage: BuildReasonMessage
) => Array<WrappedFieldsLatest<BaseFieldsLatest>>;
export type WrapSuppressedHits = (
hits: Array<estypes.SearchHit<SignalSource>>,
buildReasonMessage: BuildReasonMessage

View file

@ -35,6 +35,7 @@ import type {
import { robustGet } from './source_fields_merging/utils/robust_field_access';
import { buildAlertGroupFromSequence } from '../eql/build_alert_group_from_sequence';
import type { EqlRuleParams } from '../../rule_schema';
import { wrapHits } from '../factories';
interface SearchAfterAndBulkCreateSuppressedAlertsParams extends SearchAfterAndBulkCreateParams {
wrapSuppressedHits: WrapSuppressedHits;
@ -97,7 +98,7 @@ export const bulkCreateSuppressedAlertsInMemory = async ({
mergeSourceAndFields
);
unsuppressibleWrappedDocs = sharedParams.wrapHits(partitionedEvents[1], buildReasonMessage);
unsuppressibleWrappedDocs = wrapHits(sharedParams, partitionedEvents[1], buildReasonMessage);
suppressibleEvents = partitionedEvents[0];
}

View file

@ -39,13 +39,11 @@ import {
SPACE_IDS,
TIMESTAMP,
} from '@kbn/rule-data-utils';
import type { BulkCreate, BulkResponse, WrapHits } from '../types';
import { getCompleteRuleMock, getQueryRuleParams } from '../../rule_schema/mocks';
import type { BulkCreate, BulkResponse } from '../types';
import { getQueryRuleParams } from '../../rule_schema/mocks';
import { bulkCreateFactory } from '../factories/bulk_create_factory';
import { wrapHitsFactory } from '../factories/wrap_hits_factory';
import { ruleExecutionLogMock } from '../../rule_monitoring/mocks';
import type { BuildReasonMessage } from './reason_formatters';
import type { QueryRuleParams } from '../../rule_schema';
import { SERVER_APP_ID } from '../../../../../common/constants';
import { getSharedParamsMock } from '../__mocks__/shared_params';
@ -60,20 +58,7 @@ describe('searchAfterAndBulkCreate', () => {
const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create();
const someGuids = Array.from({ length: 13 }).map(() => uuidv4());
const sampleParams = getQueryRuleParams();
const queryCompleteRule = getCompleteRuleMock<QueryRuleParams>(sampleParams);
const inputIndex = ['auditbeat-*'];
const wrapHits: WrapHits = wrapHitsFactory({
completeRule: queryCompleteRule,
mergeStrategy: 'missingFields',
ignoreFields: {},
ignoreFieldsRegexes: [],
spaceId: 'default',
indicesToQuery: inputIndex,
alertTimestampOverride: undefined,
ruleExecutionLogger,
publicBaseUrl: 'http://testkibanabaseurl.com',
intendedTimestamp: undefined,
});
const bulkCreate: BulkCreate = bulkCreateFactory(
mockPersistenceServices.alertWithPersistence,
false,
@ -102,7 +87,6 @@ describe('searchAfterAndBulkCreate', () => {
rewrites: {
inputIndex,
bulkCreate,
wrapHits,
searchAfterSize: 1,
listClient,
},

View file

@ -10,6 +10,7 @@ import type { SearchAfterAndBulkCreateParams, SearchAfterAndBulkCreateReturnType
import { createEnrichEventsFunction } from './enrichments';
import type { SearchAfterAndBulkCreateFactoryParams } from './search_after_bulk_create_factory';
import { searchAfterAndBulkCreateFactory } from './search_after_bulk_create_factory';
import { wrapHits } from '../factories';
// search_after through documents and re-index using bulk endpoint.
export const searchAfterAndBulkCreate = async (
@ -21,7 +22,7 @@ export const searchAfterAndBulkCreate = async (
enrichedEvents,
toReturn,
}) => {
const wrappedDocs = sharedParams.wrapHits(enrichedEvents, buildReasonMessage);
const wrappedDocs = wrapHits(sharedParams, enrichedEvents, buildReasonMessage);
const bulkCreateResult = await sharedParams.bulkCreate(
wrappedDocs,

View file

@ -148,8 +148,7 @@ describe('wrapSuppressedAlerts', () => {
});
expect(transformHitToAlertMock).toHaveBeenCalledWith({
spaceId: 'default',
completeRule: sharedParams.completeRule,
sharedParams,
doc: {
fields: {
'@timestamp': [expectedTimestamp],
@ -159,16 +158,9 @@ describe('wrapSuppressedAlerts', () => {
_id: '1',
_index: 'test*',
},
mergeStrategy: 'missingFields',
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage,
indicesToQuery: ['test*'],
alertTimestampOverride: undefined,
ruleExecutionLogger,
alertUuid: expect.any(String),
publicBaseUrl: 'public-url-mock',
});
expect(wrappedAlerts[0]._source).toEqual(
expect.objectContaining({

View file

@ -36,18 +36,7 @@ export const wrapSuppressedAlerts = ({
buildReasonMessage: BuildReasonMessage;
sharedParams: SecuritySharedParams<MachineLearningRuleParams | EqlRuleParams | ThreatRuleParams>;
}): Array<WrappedFieldsLatest<BaseFieldsLatest & SuppressionFieldsLatest>> => {
const {
completeRule,
spaceId,
mergeStrategy,
inputIndex: indicesToQuery,
alertTimestampOverride,
ruleExecutionLogger,
publicBaseUrl,
intendedTimestamp,
primaryTimestamp,
secondaryTimestamp,
} = sharedParams;
const { completeRule, spaceId, primaryTimestamp, secondaryTimestamp } = sharedParams;
return events.map((event) => {
const suppressionTerms = getSuppressionTerms({
alertSuppression: completeRule?.ruleParams?.alertSuppression,
@ -65,20 +54,11 @@ export const wrapSuppressedAlerts = ({
const instanceId = objectHash([suppressionTerms, completeRule.alertId, spaceId]);
const baseAlert: BaseFieldsLatest = transformHitToAlert({
spaceId,
completeRule,
sharedParams,
doc: event,
mergeStrategy,
ignoreFields: {},
ignoreFieldsRegexes: [],
applyOverrides: true,
buildReasonMessage,
indicesToQuery,
alertTimestampOverride,
ruleExecutionLogger,
alertUuid: id,
publicBaseUrl,
intendedTimestamp,
});
return {

View file

@ -158,7 +158,7 @@ export default ({ getService }: FtrProviderContext) => {
'kibana.alert.rule.from': '2020-10-28T06:00:00.000Z',
'kibana.alert.rule.immutable': false,
'kibana.alert.rule.interval': '1h',
'kibana.alert.rule.indices': [],
'kibana.alert.rule.indices': ['ecs_compliant'],
'kibana.alert.rule.max_signals': 100,
'kibana.alert.rule.references': [],
'kibana.alert.rule.risk_score_mapping': [],