-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Security Solution][Alerts] Alert suppression time window #148868
Changes from 16 commits
d48454d
51776bc
382f0c3
bf6ec62
b210b3b
5d52a88
6743a5d
899d160
3a6d854
f39021f
7d7b504
60b8490
bea5a14
14037fd
57cb8cf
b2d2df2
feb371f
79e81a9
d2c586b
d654699
0c0b157
ce3a624
424d74b
6e6c572
969253f
9b98237
6052cc9
3f1ecb2
487f56b
5650d6f
1ca4f87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/* | ||
* 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 { | ||
ALERT_SUPPRESSION_TERMS, | ||
ALERT_SUPPRESSION_START, | ||
ALERT_SUPPRESSION_END, | ||
ALERT_SUPPRESSION_DOCS_COUNT, | ||
ALERT_INSTANCE_ID, | ||
} from '@kbn/rule-data-utils'; | ||
|
||
/* DO NOT MODIFY THIS SCHEMA TO ADD NEW FIELDS. These types represent the alerts that shipped in 8.0.0. | ||
Any changes to these types should be bug fixes so the types more accurately represent the alerts from 8.0.0. | ||
|
||
If you are adding new fields for a new release of Kibana, create a new sibling folder to this one | ||
for the version to be released and add the field(s) to the schema in that folder. | ||
|
||
Then, update `../index.ts` to import from the new folder that has the latest schemas, add the | ||
new schemas to the union of all alert schemas, and re-export the new schemas as the `*Latest` schemas. | ||
*/ | ||
|
||
export interface SuppressionFields { | ||
[ALERT_SUPPRESSION_TERMS]: Array<{ field: string; value: string | number | null }>; | ||
[ALERT_SUPPRESSION_START]: Date; | ||
[ALERT_SUPPRESSION_END]: Date; | ||
[ALERT_SUPPRESSION_DOCS_COUNT]: number; | ||
[ALERT_INSTANCE_ID]: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,13 +5,23 @@ | |
* 2.0. | ||
*/ | ||
|
||
import dateMath from '@elastic/datemath'; | ||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; | ||
import { chunk } from 'lodash'; | ||
import { ALERT_UUID, VERSION } from '@kbn/rule-data-utils'; | ||
import { chunk, partition } from 'lodash'; | ||
import { | ||
ALERT_INSTANCE_ID, | ||
ALERT_LAST_DETECTED, | ||
ALERT_SUPPRESSION_DOCS_COUNT, | ||
ALERT_SUPPRESSION_END, | ||
ALERT_UUID, | ||
TIMESTAMP, | ||
VERSION, | ||
} from '@kbn/rule-data-utils'; | ||
import { getCommonAlertFields } from './get_common_alert_fields'; | ||
import { CreatePersistenceRuleTypeWrapper } from './persistence_types'; | ||
import { errorAggregator } from './utils'; | ||
import { createGetSummarizedAlertsFn } from './create_get_summarized_alerts_fn'; | ||
import { SuppressionFields } from '../../common/schemas/8.7.0'; | ||
|
||
export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper = | ||
({ logger, ruleDataClient }) => | ||
|
@@ -94,7 +104,7 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper | |
spaceId: options.spaceId, | ||
}); | ||
} catch (e) { | ||
logger.debug('Enrichemnts failed'); | ||
logger.debug('Enrichments failed'); | ||
} | ||
} | ||
|
||
|
@@ -146,6 +156,171 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper | |
return { createdAlerts: [], errors: {}, alertsWereTruncated: false }; | ||
} | ||
}, | ||
alertWithSuppression: async ( | ||
mikecote marked this conversation as resolved.
Show resolved
Hide resolved
mikecote marked this conversation as resolved.
Show resolved
Hide resolved
|
||
alerts, | ||
suppressionWindow, | ||
enrichAlerts, | ||
currentTimeOverride | ||
) => { | ||
const ruleDataClientWriter = await ruleDataClient.getWriter({ | ||
namespace: options.spaceId, | ||
}); | ||
|
||
// Only write alerts if: | ||
// - writing is enabled | ||
// AND | ||
// - rule execution has not been cancelled due to timeout | ||
// OR | ||
// - if execution has been cancelled due to timeout, if feature flags are configured to write alerts anyway | ||
const writeAlerts = | ||
ruleDataClient.isWriteEnabled() && options.services.shouldWriteAlerts(); | ||
|
||
if (writeAlerts && alerts.length > 0) { | ||
const commonRuleFields = getCommonAlertFields(options); | ||
|
||
const suppressionWindowStart = dateMath.parse(suppressionWindow, { | ||
forceNow: currentTimeOverride, | ||
}); | ||
if (!suppressionWindowStart) { | ||
throw new Error('Failed to parse suppression window'); | ||
} | ||
|
||
const suppressionAlertSearchRequest = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we make this query work with We will need to set |
||
body: { | ||
size: alerts.length, | ||
query: { | ||
bool: { | ||
filter: [ | ||
{ | ||
range: { | ||
[TIMESTAMP]: { | ||
gte: suppressionWindowStart.toISOString(), | ||
}, | ||
}, | ||
}, | ||
{ | ||
terms: { | ||
[ALERT_INSTANCE_ID]: alerts.map( | ||
(alert) => alert._source['kibana.alert.instance.id'] | ||
), | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
collapse: { | ||
field: ALERT_INSTANCE_ID, | ||
}, | ||
sort: [ | ||
{ | ||
'@timestamp': { | ||
marshallmain marked this conversation as resolved.
Show resolved
Hide resolved
|
||
order: 'desc' as const, | ||
}, | ||
}, | ||
], | ||
}, | ||
}; | ||
|
||
const response = await ruleDataClient | ||
.getReader({ namespace: options.spaceId }) | ||
.search<typeof suppressionAlertSearchRequest, SuppressionFields>( | ||
suppressionAlertSearchRequest | ||
); | ||
|
||
const existingAlertsByInstanceId = response.hits.hits.reduce< | ||
Record<string, estypes.SearchHit<SuppressionFields>> | ||
>((acc, hit) => { | ||
acc[hit._source['kibana.alert.instance.id']] = hit; | ||
return acc; | ||
}, {}); | ||
|
||
const [duplicateAlerts, newAlerts] = partition( | ||
alerts, | ||
(alert) => | ||
existingAlertsByInstanceId[alert._source['kibana.alert.instance.id']] != null | ||
); | ||
|
||
const duplicateAlertUpdates = duplicateAlerts.flatMap((alert) => { | ||
const existingAlert = | ||
existingAlertsByInstanceId[alert._source['kibana.alert.instance.id']]; | ||
const existingDocsCount = | ||
existingAlert._source?.[ALERT_SUPPRESSION_DOCS_COUNT] ?? 0; | ||
return [ | ||
{ | ||
update: { | ||
_id: existingAlert._id, | ||
_index: existingAlert._index, | ||
require_alias: false, | ||
}, | ||
}, | ||
{ | ||
doc: { | ||
[ALERT_LAST_DETECTED]: currentTimeOverride ?? new Date(), | ||
[ALERT_SUPPRESSION_END]: alert._source[ALERT_SUPPRESSION_END], | ||
[ALERT_SUPPRESSION_DOCS_COUNT]: | ||
existingDocsCount + alert._source[ALERT_SUPPRESSION_DOCS_COUNT] + 1, | ||
}, | ||
spong marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, | ||
]; | ||
}); | ||
|
||
let enrichedAlerts = newAlerts; | ||
|
||
if (enrichAlerts) { | ||
try { | ||
enrichedAlerts = await enrichAlerts(enrichedAlerts, { | ||
spaceId: options.spaceId, | ||
}); | ||
} catch (e) { | ||
logger.debug('Enrichments failed'); | ||
} | ||
} | ||
|
||
const augmentedAlerts = enrichedAlerts.map((alert) => { | ||
return { | ||
...alert, | ||
_source: { | ||
[VERSION]: ruleDataClient.kibanaVersion, | ||
[ALERT_LAST_DETECTED]: currentTimeOverride ?? new Date(), | ||
...commonRuleFields, | ||
...alert._source, | ||
}, | ||
}; | ||
}); | ||
|
||
const newAlertCreates = augmentedAlerts.flatMap((alert) => [ | ||
{ create: { _id: alert._id } }, | ||
alert._source, | ||
]); | ||
|
||
const bulkResponse = await ruleDataClientWriter.bulk({ | ||
body: [...duplicateAlertUpdates, ...newAlertCreates], | ||
refresh: true, | ||
mikecote marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
|
||
if (bulkResponse == null) { | ||
return { createdAlerts: [], errors: {} }; | ||
} | ||
|
||
return { | ||
createdAlerts: augmentedAlerts | ||
.map((alert, idx) => { | ||
const responseItem = | ||
bulkResponse.body.items[idx + duplicateAlertUpdates.length].create; | ||
return { | ||
_id: responseItem?._id ?? '', | ||
_index: responseItem?._index ?? '', | ||
...alert._source, | ||
}; | ||
}) | ||
.filter((_, idx) => bulkResponse.body.items[idx].create?.status === 201), | ||
errors: errorAggregator(bulkResponse.body, [409]), | ||
}; | ||
} else { | ||
logger.debug('Writing is disabled.'); | ||
return { createdAlerts: [], errors: {} }; | ||
} | ||
}, | ||
}, | ||
}); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move this field to the
default_alerts_as_data.ts
file to make it available for all alerts and have it renamed tolast_detected
for consistency withstart
andend
?