[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:
Vitalii Dmyterko 2024-06-07 15:50:42 +01:00 committed by GitHub
parent cd64dc4165
commit 93c45874a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 500 additions and 48 deletions

View file

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

View file

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

View file

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

View file

@ -69,7 +69,6 @@ export const wrapSuppressedEsqlAlerts = ({
tuple,
isRuleAggregating,
index: i,
suppressionTerms,
});
const instanceId = objectHash([suppressionTerms, completeRule.alertId, spaceId]);

View file

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

View file

@ -67,7 +67,6 @@ export const wrapSuppressedNewTermsAlerts = ({
String(eventAndTerms.event._version),
`${spaceId}:${completeRule.alertId}`,
eventAndTerms.newTerms,
suppressionTerms,
]);
const baseAlert: BaseFieldsLatest = buildBulkBody(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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