mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution][Detection Engine] removes suppression terms from alert id (#184453)
## Summary - Removes suppression terms from list of properties alert id is generated. - As part of [discussion](https://github.com/elastic/kibana/pull/181926#discussion_r1593337828) we decided that alerts generated from suppression in memory do not need to have suppression terms as part of id generation. This would prevent creating duplicate alerts for different suppression configurations - flaky tests https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6171
This commit is contained in:
parent
cd64dc4165
commit
93c45874a3
13 changed files with 500 additions and 48 deletions
|
@ -142,15 +142,5 @@ describe('generateAlertId', () => {
|
|||
modifiedIdParams.completeRule.ruleParams.query = 'from packetbeat*';
|
||||
expect(id).toBe(generateAlertId(modifiedIdParams));
|
||||
});
|
||||
|
||||
it('creates id dependant on suppression terms', () => {
|
||||
modifiedIdParams.suppressionTerms = [{ field: 'agent.name', value: ['test-1'] }];
|
||||
const id1 = generateAlertId(modifiedIdParams);
|
||||
modifiedIdParams.suppressionTerms = [{ field: 'agent.name', value: ['test-2'] }];
|
||||
const id2 = generateAlertId(modifiedIdParams);
|
||||
|
||||
expect(id).not.toBe(id1);
|
||||
expect(id1).not.toBe(id2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,7 +11,6 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/types';
|
|||
|
||||
import type { CompleteRule, EsqlRuleParams } from '../../../rule_schema';
|
||||
import type { SignalSource } from '../../types';
|
||||
import type { SuppressionTerm } from '../../utils/suppression_utils';
|
||||
/**
|
||||
* Generates id for ES|QL alert.
|
||||
* Id is generated as hash of event properties and rule/space config identifiers.
|
||||
|
@ -24,7 +23,6 @@ export const generateAlertId = ({
|
|||
tuple,
|
||||
isRuleAggregating,
|
||||
index,
|
||||
suppressionTerms,
|
||||
}: {
|
||||
isRuleAggregating: boolean;
|
||||
event: estypes.SearchHit<SignalSource>;
|
||||
|
@ -36,18 +34,11 @@ export const generateAlertId = ({
|
|||
maxSignals: number;
|
||||
};
|
||||
index: number;
|
||||
suppressionTerms?: SuppressionTerm[];
|
||||
}) => {
|
||||
const ruleRunId = tuple.from.toISOString() + tuple.to.toISOString();
|
||||
|
||||
return !isRuleAggregating && event._id
|
||||
? objectHash([
|
||||
event._id,
|
||||
event._version,
|
||||
event._index,
|
||||
`${spaceId}:${completeRule.alertId}`,
|
||||
...(suppressionTerms ? [suppressionTerms] : []),
|
||||
])
|
||||
? objectHash([event._id, event._version, event._index, `${spaceId}:${completeRule.alertId}`])
|
||||
: objectHash([
|
||||
ruleRunId,
|
||||
completeRule.ruleParams.query,
|
||||
|
|
|
@ -55,10 +55,10 @@ describe('wrapSuppressedEsqlAlerts', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(alerts[0]._id).toEqual('d94fb11e6062d7dce881ea07d952a1280398663a');
|
||||
expect(alerts[0]._source[ALERT_UUID]).toEqual('d94fb11e6062d7dce881ea07d952a1280398663a');
|
||||
expect(alerts[0]._id).toEqual('ed7fbf575371c898e0f0aea48cdf0bf1865939a9');
|
||||
expect(alerts[0]._source[ALERT_UUID]).toEqual('ed7fbf575371c898e0f0aea48cdf0bf1865939a9');
|
||||
expect(alerts[0]._source[ALERT_URL]).toContain(
|
||||
'http://somekibanabaseurl.com/app/security/alerts/redirect/d94fb11e6062d7dce881ea07d952a1280398663a?index=.alerts-security.alerts-default'
|
||||
'http://somekibanabaseurl.com/app/security/alerts/redirect/ed7fbf575371c898e0f0aea48cdf0bf1865939a9?index=.alerts-security.alerts-default'
|
||||
);
|
||||
expect(alerts[0]._source[ALERT_SUPPRESSION_DOCS_COUNT]).toEqual(0);
|
||||
expect(alerts[0]._source[ALERT_INSTANCE_ID]).toEqual(
|
||||
|
|
|
@ -69,7 +69,6 @@ export const wrapSuppressedEsqlAlerts = ({
|
|||
tuple,
|
||||
isRuleAggregating,
|
||||
index: i,
|
||||
suppressionTerms,
|
||||
});
|
||||
|
||||
const instanceId = objectHash([suppressionTerms, completeRule.alertId, spaceId]);
|
||||
|
|
|
@ -48,11 +48,11 @@ describe('wrapSuppressedNewTermsAlerts', () => {
|
|||
primaryTimestamp: '@timestamp',
|
||||
});
|
||||
|
||||
expect(alerts[0]._id).toEqual('3b67aa2ebdc628afc98febc65082d2d83a116d79');
|
||||
expect(alerts[0]._source[ALERT_UUID]).toEqual('3b67aa2ebdc628afc98febc65082d2d83a116d79');
|
||||
expect(alerts[0]._id).toEqual('a36d9fe6fe4b2f65058fb1a487733275f811af58');
|
||||
expect(alerts[0]._source[ALERT_UUID]).toEqual('a36d9fe6fe4b2f65058fb1a487733275f811af58');
|
||||
expect(alerts[0]._source[ALERT_NEW_TERMS]).toEqual(['127.0.0.1']);
|
||||
expect(alerts[0]._source[ALERT_URL]).toContain(
|
||||
'http://somekibanabaseurl.com/app/security/alerts/redirect/3b67aa2ebdc628afc98febc65082d2d83a116d79?index=.alerts-security.alerts-default'
|
||||
'http://somekibanabaseurl.com/app/security/alerts/redirect/a36d9fe6fe4b2f65058fb1a487733275f811af58?index=.alerts-security.alerts-default'
|
||||
);
|
||||
expect(alerts[0]._source[ALERT_SUPPRESSION_DOCS_COUNT]).toEqual(0);
|
||||
expect(alerts[0]._source[ALERT_INSTANCE_ID]).toEqual(
|
||||
|
@ -83,10 +83,10 @@ describe('wrapSuppressedNewTermsAlerts', () => {
|
|||
primaryTimestamp: '@timestamp',
|
||||
});
|
||||
|
||||
expect(alerts[0]._id).toEqual('3e0436a03b735af12d6e5358cb36d2c3b39425a8');
|
||||
expect(alerts[0]._source[ALERT_UUID]).toEqual('3e0436a03b735af12d6e5358cb36d2c3b39425a8');
|
||||
expect(alerts[0]._id).toEqual('a36d9fe6fe4b2f65058fb1a487733275f811af58');
|
||||
expect(alerts[0]._source[ALERT_UUID]).toEqual('a36d9fe6fe4b2f65058fb1a487733275f811af58');
|
||||
expect(alerts[0]._source[ALERT_URL]).toContain(
|
||||
'http://somekibanabaseurl.com/app/security/alerts/redirect/3e0436a03b735af12d6e5358cb36d2c3b39425a8?index=.alerts-security.alerts-default'
|
||||
'http://somekibanabaseurl.com/app/security/alerts/redirect/a36d9fe6fe4b2f65058fb1a487733275f811af58?index=.alerts-security.alerts-default'
|
||||
);
|
||||
expect(alerts[0]._source[ALERT_SUPPRESSION_DOCS_COUNT]).toEqual(0);
|
||||
expect(alerts[0]._source[ALERT_INSTANCE_ID]).toEqual(
|
||||
|
@ -111,10 +111,10 @@ describe('wrapSuppressedNewTermsAlerts', () => {
|
|||
primaryTimestamp: '@timestamp',
|
||||
});
|
||||
|
||||
expect(alerts[0]._id).toEqual('f8a029df9c99e245dc83977153a0612178f3d2e8');
|
||||
expect(alerts[0]._source[ALERT_UUID]).toEqual('f8a029df9c99e245dc83977153a0612178f3d2e8');
|
||||
expect(alerts[0]._id).toEqual('f7877a31b1cc83373dbc9ba5939ebfab1db66545');
|
||||
expect(alerts[0]._source[ALERT_UUID]).toEqual('f7877a31b1cc83373dbc9ba5939ebfab1db66545');
|
||||
expect(alerts[0]._source[ALERT_URL]).toContain(
|
||||
'http://somekibanabaseurl.com/s/otherSpace/app/security/alerts/redirect/f8a029df9c99e245dc83977153a0612178f3d2e8?index=.alerts-security.alerts-otherSpace'
|
||||
'http://somekibanabaseurl.com/s/otherSpace/app/security/alerts/redirect/f7877a31b1cc83373dbc9ba5939ebfab1db66545?index=.alerts-security.alerts-otherSpace'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -132,11 +132,11 @@ describe('wrapSuppressedNewTermsAlerts', () => {
|
|||
primaryTimestamp: '@timestamp',
|
||||
});
|
||||
|
||||
expect(alerts[0]._id).toEqual('cb8684ec72592346d32839b1838e4f4080dc052e');
|
||||
expect(alerts[0]._source[ALERT_UUID]).toEqual('cb8684ec72592346d32839b1838e4f4080dc052e');
|
||||
expect(alerts[0]._id).toEqual('75e5a507a4bc48bcd983820c7fd2d9621ff4e2ea');
|
||||
expect(alerts[0]._source[ALERT_UUID]).toEqual('75e5a507a4bc48bcd983820c7fd2d9621ff4e2ea');
|
||||
expect(alerts[0]._source[ALERT_NEW_TERMS]).toEqual(['127.0.0.2']);
|
||||
expect(alerts[0]._source[ALERT_URL]).toContain(
|
||||
'http://somekibanabaseurl.com/s/otherSpace/app/security/alerts/redirect/cb8684ec72592346d32839b1838e4f4080dc052e?index=.alerts-security.alerts-otherSpace'
|
||||
'http://somekibanabaseurl.com/s/otherSpace/app/security/alerts/redirect/75e5a507a4bc48bcd983820c7fd2d9621ff4e2ea?index=.alerts-security.alerts-otherSpace'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -67,7 +67,6 @@ export const wrapSuppressedNewTermsAlerts = ({
|
|||
String(eventAndTerms.event._version),
|
||||
`${spaceId}:${completeRule.alertId}`,
|
||||
eventAndTerms.newTerms,
|
||||
suppressionTerms,
|
||||
]);
|
||||
|
||||
const baseAlert: BaseFieldsLatest = buildBulkBody(
|
||||
|
|
|
@ -82,12 +82,7 @@ export const wrapSuppressedThresholdALerts = ({
|
|||
|
||||
const suppressedValues = sortBy(Object.entries(bucket.key).map(([_, value]) => value));
|
||||
|
||||
const id = objectHash([
|
||||
hit._index,
|
||||
hit._id,
|
||||
`${spaceId}:${completeRule.alertId}`,
|
||||
suppressedValues,
|
||||
]);
|
||||
const id = objectHash([hit._index, hit._id, `${spaceId}:${completeRule.alertId}`]);
|
||||
|
||||
const instanceId = objectHash([suppressedValues, completeRule.alertId, spaceId]);
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import type { CompleteRule } from '../../rule_schema';
|
|||
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
|
||||
import { buildBulkBody } from '../factories/utils/build_bulk_body';
|
||||
import { getSuppressionAlertFields, getSuppressionTerms } from './suppression_utils';
|
||||
import { generateId } from './utils';
|
||||
|
||||
import type { BuildReasonMessage } from './reason_formatters';
|
||||
|
||||
|
@ -59,12 +60,12 @@ export const wrapSuppressedAlerts = ({
|
|||
fields: event.fields,
|
||||
});
|
||||
|
||||
const id = objectHash([
|
||||
const id = generateId(
|
||||
event._index,
|
||||
event._id,
|
||||
`${spaceId}:${completeRule.alertId}`,
|
||||
suppressionTerms,
|
||||
]);
|
||||
String(event._version),
|
||||
`${spaceId}:${completeRule.alertId}`
|
||||
);
|
||||
|
||||
const instanceId = objectHash([suppressionTerms, completeRule.alertId, spaceId]);
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import expect from 'expect';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
import { EqlRuleCreateProps } from '@kbn/security-solution-plugin/common/api/detection_engine';
|
||||
import {
|
||||
ALERT_SUPPRESSION_START,
|
||||
|
@ -255,6 +257,96 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
);
|
||||
});
|
||||
|
||||
it('deduplicates new alerts if they were previously created without suppression', async () => {
|
||||
const id = uuidv4();
|
||||
const firstTimestamp = new Date().toISOString();
|
||||
|
||||
const ruleWithoutSuppression: EqlRuleCreateProps = {
|
||||
...getEqlRuleForAlertTesting(['ecs_compliant']),
|
||||
query: getQuery(id),
|
||||
from: 'now-35m',
|
||||
interval: '30m',
|
||||
};
|
||||
const alertSuppression = {
|
||||
group_by: ['host.name'],
|
||||
duration: {
|
||||
value: 300,
|
||||
unit: 'm' as const,
|
||||
},
|
||||
missing_fields_strategy: 'suppress',
|
||||
};
|
||||
|
||||
const firstDocument = {
|
||||
id,
|
||||
'@timestamp': firstTimestamp,
|
||||
host: {
|
||||
name: 'host-a',
|
||||
},
|
||||
};
|
||||
await indexListOfSourceDocuments([firstDocument, firstDocument]);
|
||||
|
||||
const createdRule = await createRule(supertest, log, ruleWithoutSuppression);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
expect(alerts.hits.hits).toHaveLength(2);
|
||||
// alert does not have suppression properties
|
||||
alerts.hits.hits.forEach((previewAlert) => {
|
||||
const source = previewAlert._source;
|
||||
expect(source).toHaveProperty('id', id);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_DOCS_COUNT);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_END);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_TERMS);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_DOCS_COUNT);
|
||||
});
|
||||
|
||||
const secondTimestamp = new Date().toISOString();
|
||||
const secondDocument = {
|
||||
id,
|
||||
'@timestamp': secondTimestamp,
|
||||
host: {
|
||||
name: 'host-a',
|
||||
},
|
||||
};
|
||||
|
||||
await indexListOfSourceDocuments([secondDocument, secondDocument]);
|
||||
|
||||
// update the rule to include suppression
|
||||
await patchRule(supertest, log, {
|
||||
id: createdRule.id,
|
||||
alert_suppression: alertSuppression,
|
||||
enabled: false,
|
||||
});
|
||||
await patchRule(supertest, log, { id: createdRule.id, enabled: true });
|
||||
|
||||
const afterTimestamp = new Date();
|
||||
const secondAlerts = await getOpenAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
createdRule,
|
||||
RuleExecutionStatusEnum.succeeded,
|
||||
undefined,
|
||||
afterTimestamp
|
||||
);
|
||||
|
||||
expect(secondAlerts.hits.hits.length).toEqual(3);
|
||||
|
||||
const sortedAlerts = sortBy(secondAlerts.hits.hits, ALERT_ORIGINAL_TIME);
|
||||
|
||||
// third alert is generated with suppression
|
||||
expect(sortedAlerts[2]._source).toEqual(
|
||||
expect.objectContaining({
|
||||
[ALERT_SUPPRESSION_TERMS]: [
|
||||
{
|
||||
field: 'host.name',
|
||||
value: ['host-a'],
|
||||
},
|
||||
],
|
||||
[ALERT_ORIGINAL_TIME]: secondTimestamp,
|
||||
[ALERT_SUPPRESSION_DOCS_COUNT]: 1,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('does not suppress alerts when suppression duration is less than rule interval', async () => {
|
||||
const id = uuidv4();
|
||||
const firstTimestamp = '2020-10-28T05:45:00.000Z';
|
||||
|
|
|
@ -256,6 +256,95 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
);
|
||||
});
|
||||
|
||||
it('deduplicates new alerts if they were previously created without suppression', async () => {
|
||||
const id = uuidv4();
|
||||
const firstTimestamp = new Date().toISOString();
|
||||
|
||||
const ruleWithoutSuppression: EsqlRuleCreateProps = {
|
||||
...getCreateEsqlRulesSchemaMock('rule-1', true),
|
||||
query: getNonAggRuleQueryWithMetadata(id),
|
||||
from: 'now-35m',
|
||||
interval: '30m',
|
||||
};
|
||||
const alertSuppression = {
|
||||
group_by: ['host.name'],
|
||||
duration: {
|
||||
value: 300,
|
||||
unit: 'm' as const,
|
||||
},
|
||||
missing_fields_strategy: 'suppress',
|
||||
};
|
||||
|
||||
const firstExecutionDocuments = [
|
||||
{
|
||||
host: { name: 'host-0' },
|
||||
id,
|
||||
'@timestamp': firstTimestamp,
|
||||
},
|
||||
];
|
||||
|
||||
await indexListOfDocuments(firstExecutionDocuments);
|
||||
|
||||
const createdRule = await createRule(supertest, log, ruleWithoutSuppression);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
expect(alerts.hits.hits).toHaveLength(1);
|
||||
// alert does not have suppression properties
|
||||
alerts.hits.hits.forEach((previewAlert) => {
|
||||
const source = previewAlert._source;
|
||||
expect(source).toHaveProperty('id', id);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_DOCS_COUNT);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_END);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_TERMS);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_DOCS_COUNT);
|
||||
});
|
||||
|
||||
const secondTimestamp = new Date().toISOString();
|
||||
const secondExecutionDocument = {
|
||||
host: { name: 'host-0' },
|
||||
id,
|
||||
'@timestamp': secondTimestamp,
|
||||
};
|
||||
|
||||
await indexListOfDocuments([secondExecutionDocument, secondExecutionDocument]);
|
||||
|
||||
// update the rule to include suppression
|
||||
await patchRule(supertest, log, {
|
||||
id: createdRule.id,
|
||||
alert_suppression: alertSuppression,
|
||||
enabled: false,
|
||||
});
|
||||
await patchRule(supertest, log, { id: createdRule.id, enabled: true });
|
||||
|
||||
const afterTimestamp = new Date();
|
||||
const secondAlerts = await getOpenAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
createdRule,
|
||||
RuleExecutionStatusEnum.succeeded,
|
||||
undefined,
|
||||
afterTimestamp
|
||||
);
|
||||
|
||||
expect(secondAlerts.hits.hits.length).toEqual(2);
|
||||
|
||||
const sortedAlerts = sortBy(secondAlerts.hits.hits, ALERT_ORIGINAL_TIME);
|
||||
|
||||
// second alert is generated with suppression
|
||||
expect(sortedAlerts[1]._source).toEqual(
|
||||
expect.objectContaining({
|
||||
[ALERT_SUPPRESSION_TERMS]: [
|
||||
{
|
||||
field: 'host.name',
|
||||
value: 'host-0',
|
||||
},
|
||||
],
|
||||
[ALERT_ORIGINAL_TIME]: secondTimestamp,
|
||||
[ALERT_SUPPRESSION_DOCS_COUNT]: 1,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT suppress alerts when suppression period is less than rule interval', async () => {
|
||||
const id = uuidv4();
|
||||
const firstTimestamp = '2020-10-28T05:45:00.000Z';
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import expect from 'expect';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
import {
|
||||
ALERT_SUPPRESSION_START,
|
||||
|
@ -372,6 +373,109 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
);
|
||||
});
|
||||
|
||||
it('deduplicates new alerts if they were previously created without suppression', async () => {
|
||||
const id = uuidv4();
|
||||
const firstTimestamp = new Date().toISOString();
|
||||
|
||||
await eventsFiller({ id, count: eventsCount, timestamp: [firstTimestamp] });
|
||||
await threatsFiller({ id, count: threatsCount, timestamp: firstTimestamp });
|
||||
|
||||
const firstDocument = {
|
||||
id,
|
||||
'@timestamp': firstTimestamp,
|
||||
host: {
|
||||
name: 'host-a',
|
||||
},
|
||||
};
|
||||
await indexListOfSourceDocuments([firstDocument]);
|
||||
|
||||
await addThreatDocuments({
|
||||
id,
|
||||
timestamp: firstTimestamp,
|
||||
fields: {
|
||||
host: {
|
||||
name: 'host-a',
|
||||
},
|
||||
},
|
||||
count: 1,
|
||||
});
|
||||
|
||||
const ruleWithoutSuppression: ThreatMatchRuleCreateProps = {
|
||||
...indicatorMatchRule(id),
|
||||
from: 'now-35m',
|
||||
interval: '30m',
|
||||
};
|
||||
const alertSuppression = {
|
||||
group_by: ['host.name'],
|
||||
duration: {
|
||||
value: 300,
|
||||
unit: 'm' as const,
|
||||
},
|
||||
missing_fields_strategy: 'suppress',
|
||||
};
|
||||
|
||||
const createdRule = await createRule(supertest, log, ruleWithoutSuppression);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
expect(alerts.hits.hits).toHaveLength(1);
|
||||
// alert does not have suppression properties
|
||||
alerts.hits.hits.forEach((previewAlert) => {
|
||||
const source = previewAlert._source;
|
||||
expect(source).toHaveProperty('id', id);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_DOCS_COUNT);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_END);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_TERMS);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_DOCS_COUNT);
|
||||
});
|
||||
|
||||
const secondTimestamp = new Date().toISOString();
|
||||
const secondDocument = {
|
||||
id,
|
||||
'@timestamp': secondTimestamp,
|
||||
host: {
|
||||
name: 'host-a',
|
||||
},
|
||||
};
|
||||
|
||||
await indexListOfSourceDocuments([secondDocument, secondDocument]);
|
||||
|
||||
// update the rule to include suppression
|
||||
await patchRule(supertest, log, {
|
||||
id: createdRule.id,
|
||||
alert_suppression: alertSuppression,
|
||||
enabled: false,
|
||||
});
|
||||
await patchRule(supertest, log, { id: createdRule.id, enabled: true });
|
||||
|
||||
const afterTimestamp = new Date();
|
||||
const secondAlerts = await getOpenAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
createdRule,
|
||||
RuleExecutionStatusEnum.succeeded,
|
||||
undefined,
|
||||
afterTimestamp
|
||||
);
|
||||
|
||||
expect(secondAlerts.hits.hits.length).toEqual(2);
|
||||
|
||||
const sortedAlerts = sortBy(secondAlerts.hits.hits, ALERT_ORIGINAL_TIME);
|
||||
|
||||
// second alert is generated with suppression
|
||||
expect(sortedAlerts[1]._source).toEqual(
|
||||
expect.objectContaining({
|
||||
[ALERT_SUPPRESSION_TERMS]: [
|
||||
{
|
||||
field: 'host.name',
|
||||
value: ['host-a'],
|
||||
},
|
||||
],
|
||||
[ALERT_ORIGINAL_TIME]: secondTimestamp,
|
||||
[ALERT_SUPPRESSION_DOCS_COUNT]: 1,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT suppress alerts when suppression period is less than rule interval', async () => {
|
||||
const id = uuidv4();
|
||||
const firstTimestamp = '2020-10-28T05:45:00.000Z';
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import expect from 'expect';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
import {
|
||||
ALERT_SUPPRESSION_START,
|
||||
|
@ -276,6 +277,105 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
);
|
||||
});
|
||||
|
||||
it('deduplicates new alerts if they were previously created without suppression', async () => {
|
||||
const id = uuidv4();
|
||||
const firstTimestamp = new Date().toISOString();
|
||||
|
||||
const ruleWithoutSuppression: NewTermsRuleCreateProps = {
|
||||
...getCreateNewTermsRulesSchemaMock(id, true),
|
||||
new_terms_fields: ['host.ip'],
|
||||
index: ['ecs_compliant'],
|
||||
history_window_start: historicalWindowStart,
|
||||
from: 'now-35m',
|
||||
interval: '30m',
|
||||
query: `id: "${id}"`,
|
||||
};
|
||||
const alertSuppression = {
|
||||
group_by: ['host.name'],
|
||||
duration: {
|
||||
value: 300,
|
||||
unit: 'm' as const,
|
||||
},
|
||||
missing_fields_strategy: 'suppress',
|
||||
};
|
||||
|
||||
const firstExecutionDocuments = [
|
||||
{
|
||||
host: { name: 'host-0', ip: '127.0.0.3' },
|
||||
id,
|
||||
'@timestamp': firstTimestamp,
|
||||
},
|
||||
];
|
||||
|
||||
await indexListOfDocuments(firstExecutionDocuments);
|
||||
|
||||
const createdRule = await createRule(supertest, log, ruleWithoutSuppression);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
expect(alerts.hits.hits).toHaveLength(1);
|
||||
// alert does not have suppression properties
|
||||
alerts.hits.hits.forEach((previewAlert) => {
|
||||
const source = previewAlert._source;
|
||||
expect(source).toHaveProperty('id', id);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_DOCS_COUNT);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_END);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_TERMS);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_DOCS_COUNT);
|
||||
});
|
||||
|
||||
const secondTimestamp = new Date().toISOString();
|
||||
const secondExecutionDocuments = [
|
||||
{
|
||||
host: { name: 'host-0', ip: '127.0.0.5' },
|
||||
id,
|
||||
'@timestamp': secondTimestamp,
|
||||
},
|
||||
{
|
||||
host: { name: 'host-0', ip: '127.0.0.6' },
|
||||
id,
|
||||
'@timestamp': secondTimestamp,
|
||||
},
|
||||
];
|
||||
|
||||
await indexListOfDocuments(secondExecutionDocuments);
|
||||
|
||||
// update the rule to include suppression
|
||||
await patchRule(supertest, log, {
|
||||
id: createdRule.id,
|
||||
alert_suppression: alertSuppression,
|
||||
enabled: false,
|
||||
});
|
||||
await patchRule(supertest, log, { id: createdRule.id, enabled: true });
|
||||
|
||||
const afterTimestamp = new Date();
|
||||
const secondAlerts = await getOpenAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
createdRule,
|
||||
RuleExecutionStatusEnum.succeeded,
|
||||
undefined,
|
||||
afterTimestamp
|
||||
);
|
||||
|
||||
expect(secondAlerts.hits.hits.length).toEqual(2);
|
||||
|
||||
const sortedAlerts = sortBy(secondAlerts.hits.hits, ALERT_ORIGINAL_TIME);
|
||||
|
||||
// second alert is generated with suppression
|
||||
expect(sortedAlerts[1]._source).toEqual(
|
||||
expect.objectContaining({
|
||||
[ALERT_SUPPRESSION_TERMS]: [
|
||||
{
|
||||
field: 'host.name',
|
||||
value: ['host-0'],
|
||||
},
|
||||
],
|
||||
[ALERT_ORIGINAL_TIME]: secondTimestamp,
|
||||
[ALERT_SUPPRESSION_DOCS_COUNT]: 1,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT suppress alerts when suppression period is less than rule interval', async () => {
|
||||
const id = uuidv4();
|
||||
const firstTimestamp = '2020-10-28T05:45:00.000Z';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import expect from 'expect';
|
||||
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import {
|
||||
ALERT_SUPPRESSION_START,
|
||||
ALERT_SUPPRESSION_END,
|
||||
|
@ -24,9 +24,11 @@ import { RuleExecutionStatusEnum } from '@kbn/security-solution-plugin/common/ap
|
|||
import { ENABLE_ASSET_CRITICALITY_SETTING } from '@kbn/security-solution-plugin/common/constants';
|
||||
|
||||
import { ALERT_ORIGINAL_TIME } from '@kbn/security-solution-plugin/common/field_maps/field_names';
|
||||
import { AlertSuppression } from '@kbn/security-solution-plugin/common/api/detection_engine/model/rule_schema';
|
||||
import { createRule } from '../../../../../../../common/utils/security_solution';
|
||||
import {
|
||||
getAlerts,
|
||||
getOpenAlerts,
|
||||
getPreviewAlerts,
|
||||
getThresholdRuleForAlertTesting,
|
||||
previewRule,
|
||||
|
@ -248,6 +250,96 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
);
|
||||
});
|
||||
|
||||
it('deduplicates new alerts if they were previously created without suppression', async () => {
|
||||
const id = uuidv4();
|
||||
const firstTimestamp = new Date().toISOString();
|
||||
const firstDocument = {
|
||||
id,
|
||||
'@timestamp': firstTimestamp,
|
||||
agent: {
|
||||
name: 'agent-1',
|
||||
},
|
||||
};
|
||||
await indexListOfDocuments([firstDocument]);
|
||||
|
||||
const ruleWithoutSuppression: ThresholdRuleCreateProps = {
|
||||
...getThresholdRuleForAlertTesting(['ecs_compliant']),
|
||||
query: `id:${id}`,
|
||||
threshold: {
|
||||
field: ['agent.name'],
|
||||
value: 1,
|
||||
},
|
||||
from: 'now-35m',
|
||||
interval: '30m',
|
||||
};
|
||||
const alertSuppression = {
|
||||
duration: {
|
||||
value: 300,
|
||||
unit: 'm',
|
||||
},
|
||||
};
|
||||
|
||||
const createdRule = await createRule(supertest, log, ruleWithoutSuppression);
|
||||
const alerts = await getOpenAlerts(supertest, log, es, createdRule);
|
||||
expect(alerts.hits.hits).toHaveLength(1);
|
||||
// alert does not have suppression properties
|
||||
alerts.hits.hits.forEach((previewAlert) => {
|
||||
const source = previewAlert._source;
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_DOCS_COUNT);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_END);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_TERMS);
|
||||
expect(source).not.toHaveProperty(ALERT_SUPPRESSION_DOCS_COUNT);
|
||||
});
|
||||
|
||||
const secondTimestamp = new Date().toISOString();
|
||||
const secondDocument = {
|
||||
id,
|
||||
'@timestamp': secondTimestamp,
|
||||
agent: {
|
||||
name: 'agent-1',
|
||||
},
|
||||
};
|
||||
|
||||
await indexListOfDocuments([secondDocument, secondDocument]);
|
||||
|
||||
// update the rule to include suppression
|
||||
await patchRule(supertest, log, {
|
||||
id: createdRule.id,
|
||||
alert_suppression: alertSuppression as AlertSuppression,
|
||||
enabled: false,
|
||||
});
|
||||
await patchRule(supertest, log, { id: createdRule.id, enabled: true });
|
||||
|
||||
const afterTimestamp = new Date();
|
||||
const secondAlerts = await getOpenAlerts(
|
||||
supertest,
|
||||
log,
|
||||
es,
|
||||
createdRule,
|
||||
RuleExecutionStatusEnum.succeeded,
|
||||
undefined,
|
||||
afterTimestamp
|
||||
);
|
||||
|
||||
expect(secondAlerts.hits.hits.length).toEqual(2);
|
||||
|
||||
const sortedAlerts = sortBy(secondAlerts.hits.hits, ALERT_ORIGINAL_TIME);
|
||||
|
||||
// second alert is generated with suppression
|
||||
expect(sortedAlerts[1]._source).toEqual(
|
||||
expect.objectContaining({
|
||||
[ALERT_SUPPRESSION_TERMS]: [
|
||||
{
|
||||
field: 'agent.name',
|
||||
value: 'agent-1',
|
||||
},
|
||||
],
|
||||
[ALERT_ORIGINAL_TIME]: secondTimestamp,
|
||||
[ALERT_SUPPRESSION_DOCS_COUNT]: 0,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate an alert per rule run when duration is less than rule interval', async () => {
|
||||
const id = uuidv4();
|
||||
const timestamp = '2020-10-28T05:45:00.000Z';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue