[8.18] [Entity Analytics] Consider Closed alerts for Risk Scoring (#193667) (#210941)

# Backport

This will backport the following commits from `main` to `8.18`:
- [[Entity Analytics] Consider Closed alerts for Risk Scoring
(#193667)](https://github.com/elastic/kibana/pull/193667)

<!--- Backport version: 9.6.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Abhishek
Bhatia","email":"117628830+abhishekbhatia1710@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-10-08T07:28:54Z","message":"[Entity
Analytics] Consider Closed alerts for Risk Scoring (#193667)\n\n##
Summary\r\n\r\n- The changes included in this PR allows the alerts in
closed state to\r\nbe included in risk score calculation.\r\n- It also
includes the changes to backfill existing data with the\r\nrequired key
so that older alerts could also be considered for risk\r\nscore
calculation if need be.\r\n- Unit tests and integration tests are also
included for the\r\nchanges.Tests for backfill changes are not included
in this PR\r\n\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are
not applicable to this PR.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n\r\n\r\n### For maintainers\r\n\r\n-
[ ] This was checked for breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"93f03e5939c897c620b36595e5fcc67e74340e38","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","backport","v9.0.0","Feature:Entity
Analytics","Team:Entity
Analytics","backport:version","v8.18.0","v9.1.0","v8.19.0"],"title":"[Entity
Analytics] Consider Closed alerts for Risk
Scoring","number":193667,"url":"https://github.com/elastic/kibana/pull/193667","mergeCommit":{"message":"[Entity
Analytics] Consider Closed alerts for Risk Scoring (#193667)\n\n##
Summary\r\n\r\n- The changes included in this PR allows the alerts in
closed state to\r\nbe included in risk score calculation.\r\n- It also
includes the changes to backfill existing data with the\r\nrequired key
so that older alerts could also be considered for risk\r\nscore
calculation if need be.\r\n- Unit tests and integration tests are also
included for the\r\nchanges.Tests for backfill changes are not included
in this PR\r\n\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are
not applicable to this PR.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n\r\n\r\n### For maintainers\r\n\r\n-
[ ] This was checked for breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"93f03e5939c897c620b36595e5fcc67e74340e38"}},"sourceBranch":"main","suggestedTargetBranches":["8.18","8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/193667","number":193667,"mergeCommit":{"message":"[Entity
Analytics] Consider Closed alerts for Risk Scoring (#193667)\n\n##
Summary\r\n\r\n- The changes included in this PR allows the alerts in
closed state to\r\nbe included in risk score calculation.\r\n- It also
includes the changes to backfill existing data with the\r\nrequired key
so that older alerts could also be considered for risk\r\nscore
calculation if need be.\r\n- Unit tests and integration tests are also
included for the\r\nchanges.Tests for backfill changes are not included
in this PR\r\n\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are
not applicable to this PR.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n\r\n\r\n### For maintainers\r\n\r\n-
[ ] This was checked for breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"93f03e5939c897c620b36595e5fcc67e74340e38"}},{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.1","label":"v9.1.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.19","label":"v8.19.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Abhishek Bhatia <117628830+abhishekbhatia1710@users.noreply.github.com>
This commit is contained in:
Jared Burgett 2025-02-12 23:50:13 -06:00 committed by GitHub
parent 6a55095236
commit 930465848e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 150 additions and 7 deletions

View file

@ -887,6 +887,7 @@
"alertSampleSizePerShard",
"dataViewId",
"enabled",
"excludeAlertStatuses",
"filter",
"identifierType",
"interval",

View file

@ -2945,6 +2945,9 @@
"enabled": {
"type": "boolean"
},
"excludeAlertStatuses": {
"type": "keyword"
},
"filter": {
"dynamic": false,
"properties": {}

View file

@ -148,7 +148,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"policy-settings-protection-updates-note": "33924bb246f9e5bcb876109cc83e3c7a28308352",
"product-doc-install-status": "ca6e96840228e4cc2f11bae24a0797f4f7238c8c",
"query": "501bece68f26fe561286a488eabb1a8ab12f1137",
"risk-engine-configuration": "aea0c371a462e6d07c3ceb3aff11891b47feb09d",
"risk-engine-configuration": "bab237d09c2e7189dddddcb1b28f19af69755efb",
"rules-settings": "ba57ef1881b3dcbf48fbfb28902d8f74442190b2",
"sample-data-telemetry": "37441b12f5b0159c2d6d5138a494c9f440e950b5",
"search": "0aa6eefb37edd3145be340a8b67779c2ca578b22",

View file

@ -55,6 +55,12 @@ export const RiskScoresPreviewRequest = z.object({
*/
range: DateRange.optional(),
weights: RiskScoreWeights.optional(),
/**
* A list of alert statuses to exclude from the risk score calculation. If unspecified, all alert statuses are included.
*/
excludeAlertStatuses: z
.array(z.enum(['open', 'closed', 'in-progress', 'acknowledged']))
.optional(),
});
export type RiskScoresPreviewResponse = z.infer<typeof RiskScoresPreviewResponse>;

View file

@ -58,6 +58,17 @@ components:
description: Defines the time period over which scores will be evaluated. If unspecified, a range of `[now, now-30d]` will be used.
weights:
$ref: '../common/common.schema.yaml#/components/schemas/RiskScoreWeights'
excludeAlertStatuses:
description: A list of alert statuses to exclude from the risk score calculation. If unspecified, all alert statuses are included.
type: array
items:
type: string
enum:
- open
- closed
- in-progress
- acknowledged
RiskScoresPreviewResponse:
type: object

View file

@ -45,6 +45,9 @@ export const riskEngineConfigurationTypeMappings: SavedObjectsType['mappings'] =
},
},
},
excludeAlertStatuses: {
type: 'keyword',
},
},
};
@ -59,6 +62,28 @@ const version1: SavedObjectsModelVersion = {
],
};
const version2: SavedObjectsModelVersion = {
changes: [
{
type: 'mappings_addition',
addedMappings: {
excludeAlertStatuses: { type: 'keyword' },
},
},
{
type: 'data_backfill',
backfillFn: (document) => {
return {
attributes: {
...document.attributes,
excludeAlertStatuses: document.attributes.excludeAlertStatuses || ['closed'],
},
};
},
},
],
};
export const riskEngineConfigurationType: SavedObjectsType = {
name: riskEngineConfigurationTypeName,
indexPattern: SECURITY_SOLUTION_SAVED_OBJECT_INDEX,
@ -67,5 +92,6 @@ export const riskEngineConfigurationType: SavedObjectsType = {
mappings: riskEngineConfigurationTypeMappings,
modelVersions: {
1: version1,
2: version2,
},
};

View file

@ -14,6 +14,8 @@ import { calculateRiskScoresMock } from './calculate_risk_scores.mock';
import { mockGlobalState } from '../../../../public/common/mock';
import { EntityType } from '../../../../common/search_strategy';
import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names';
describe('calculateRiskScores()', () => {
let params: Parameters<typeof calculateRiskScores>[0];
let esClient: ElasticsearchClient;
@ -149,6 +151,41 @@ describe('calculateRiskScores()', () => {
);
});
});
describe('excludeAlertStatuses', () => {
it('should not add the filter when excludeAlertStatuses is empty', async () => {
params = { ...params, excludeAlertStatuses: [] };
await calculateRiskScores(params);
expect(
(esClient.search as jest.Mock).mock.calls[0][0].query.function_score.query.bool.filter
).toEqual(
expect.not.arrayContaining([
{
bool: {
must_not: { terms: { [ALERT_WORKFLOW_STATUS]: params.excludeAlertStatuses } },
},
},
])
);
});
it('should add the filter when excludeAlertStatuses is not empty', async () => {
esClient.search as jest.Mock;
params = { ...params, excludeAlertStatuses: ['closed'] };
await calculateRiskScores(params);
expect(
(esClient.search as jest.Mock).mock.calls[0][0].query.function_score.query.bool.filter
).toEqual(
expect.arrayContaining([
{
bool: {
must_not: { terms: { [ALERT_WORKFLOW_STATUS]: params.excludeAlertStatuses } },
},
},
])
);
});
});
});
describe('outputs', () => {

View file

@ -98,7 +98,7 @@ const formatForResponse = ({
};
};
const filterFromRange = (range: CalculateScoresParams['range']): QueryDslQueryContainer => ({
export const filterFromRange = (range: CalculateScoresParams['range']): QueryDslQueryContainer => ({
range: { '@timestamp': { lt: range.end, gte: range.start } },
});
@ -221,6 +221,7 @@ export const calculateRiskScores = async ({
weights,
alertSampleSizePerShard = 10_000,
experimentalFeatures,
excludeAlertStatuses = [],
}: {
assetCriticalityService: AssetCriticalityService;
esClient: ElasticsearchClient;
@ -230,11 +231,12 @@ export const calculateRiskScores = async ({
withSecuritySpan('calculateRiskScores', async () => {
const now = new Date().toISOString();
const scriptedMetricPainless = await getPainlessScripts();
const filter = [
filterFromRange(range),
{ bool: { must_not: { term: { [ALERT_WORKFLOW_STATUS]: 'closed' } } } },
{ exists: { field: ALERT_RISK_SCORE } },
];
const filter = [filterFromRange(range), { exists: { field: ALERT_RISK_SCORE } }];
if (excludeAlertStatuses.length > 0) {
filter.push({
bool: { must_not: { terms: { [ALERT_WORKFLOW_STATUS]: excludeAlertStatuses } } },
});
}
if (!isEmpty(userFilter)) {
filter.push(userFilter as QueryDslQueryContainer);
}

View file

@ -66,6 +66,7 @@ export const riskScorePreviewRoute = (
filter,
range: userRange,
weights,
excludeAlertStatuses,
} = request.body;
const entityAnalyticsConfig = await riskScoreService.getConfigurationWithDefaults(
@ -96,6 +97,7 @@ export const riskScorePreviewRoute = (
runtimeMappings,
weights,
alertSampleSizePerShard,
excludeAlertStatuses,
});
securityContext.getAuditLogger()?.log({

View file

@ -94,6 +94,7 @@ export interface CalculateScoresParams {
runtimeMappings: MappingRuntimeFields;
weights?: RiskScoreWeights;
alertSampleSizePerShard?: number;
excludeAlertStatuses?: string[];
}
export interface CalculateAndPersistScoresParams {

View file

@ -257,6 +257,60 @@ export default ({ getService }: FtrProviderContext): void => {
},
]);
});
it('calculates risk from 5 alerts, all in closed state, all for the same host', async () => {
const documentId = uuidv4();
const doc = buildDocument(
{ host: { name: 'host-1' }, kibana: { alert: { workflow_status: 'closed' } } },
documentId
);
await indexListOfDocuments(Array(10).fill(doc));
const body = await getRiskScoreAfterRuleCreationAndExecution(documentId, {
alerts: 5,
});
expect(sanitizeScores(body.scores.host!)).to.eql([
{
calculated_level: 'Unknown',
calculated_score: 41.90206636025764,
calculated_score_norm: 16.163426307767953,
category_1_count: 10,
category_1_score: 16.163426307767953,
id_field: 'host.name',
id_value: 'host-1',
},
]);
});
it('calculates risk from 10 alerts, some in closed state, some in open state, all for the same host', async () => {
const documentId = uuidv4();
const docStatusClosed = buildDocument(
{ host: { name: 'host-1' }, kibana: { alert: { workflow_status: 'closed' } } },
documentId
);
const docStatusOpen = buildDocument(
{ host: { name: 'host-1' }, kibana: { alert: { workflow_status: 'open' } } },
documentId
);
await indexListOfDocuments(Array(5).fill(docStatusClosed));
await indexListOfDocuments(Array(5).fill(docStatusOpen));
const body = await getRiskScoreAfterRuleCreationAndExecution(documentId, {
alerts: 10,
});
expect(sanitizeScores(body.scores.host!)).to.eql([
{
calculated_level: 'Unknown',
calculated_score: 41.90206636025764,
calculated_score_norm: 16.163426307767953,
category_1_count: 10,
category_1_score: 16.163426307767953,
id_field: 'host.name',
id_value: 'host-1',
},
]);
});
});
context('with a rule generating alerts with risk_score of 100', () => {