Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
enrichmentTimes: result.enrichmentTimes.concat(runResult.enrichmentTimes),
createdSignals,
createdSignalsCount: createdSignals.length,
alertsCandidateCount: runResult.alertsCandidateCount,
suppressedAlertsCount: runResult.suppressedAlertsCount,
totalEventsFound:
(result.totalEventsFound ?? 0) + (runResult.totalEventsFound ?? 0),
Expand Down Expand Up @@ -476,6 +477,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
? Math.round(sum(result.enrichmentTimes.map(Number)))
: undefined,
frozen_indices_queried_count: frozenIndicesQueriedCount,
alerts_candidate_count: result.alertsCandidateCount,
alerts_suppressed_count: suppressedAlertsCount,
gap_duration_s:
gap && remainingGap ? Math.round(remainingGap.asSeconds()) : undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ export const eqlExecutor = async ({
const { events, sequences } = response.hits;

if (events) {
// Collect rule execution metrics
result.totalEventsFound = events.length;
result.alertsCandidateCount = events.length;

if (
isAlertSuppressionActive &&
alertSuppressionTypeGuard(completeRule.ruleParams.alertSuppression)
Expand All @@ -163,7 +166,10 @@ export const eqlExecutor = async ({
newSignals = wrapHits(sharedParams, events, buildReasonMessageForEqlAlert);
}
} else if (sequences) {
// Collect rule execution metrics
result.totalEventsFound = sequences.length;
result.alertsCandidateCount = sequences.length;

if (
isAlertSuppressionActive &&
alertSuppressionTypeGuard(completeRule.ruleParams.alertSuppression)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ export const esqlExecutor = async ({
};
});

// Collect rule execution metrics
result.alertsCandidateCount = syntheticHits.length;

if (
isAlertSuppressionActive &&
alertSuppressionTypeGuard(completeRule.ruleParams.alertSuppression)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import moment from 'moment';

import { get, isEmpty } from 'lodash';
import { get, isEmpty, sum } from 'lodash';

import type { ThreatMapping } from '../../../../../../common/api/detection_engine/model/rule_schema';
import { TelemetryChannel } from '../../../../telemetry/types';
Expand Down Expand Up @@ -96,6 +96,7 @@ export const combineResults = (
currentResult.searchAfterTimes,
newResult.searchAfterTimes
),
alertsCandidateCount: sum([currentResult.alertsCandidateCount, newResult.alertsCandidateCount]),
createdSignalsCount: currentResult.createdSignalsCount + newResult.createdSignalsCount,
createdSignals: [...currentResult.createdSignals, ...newResult.createdSignals],
warningMessages: [...currentResult.warningMessages, ...newResult.warningMessages],
Expand Down Expand Up @@ -124,6 +125,7 @@ export const combineConcurrentResults = (
searchAfterTimes: [maxSearchAfterTime],
bulkCreateTimes: [maxBulkCreateTimes],
enrichmentTimes: [maxEnrichmentTimes],
alertsCandidateCount: sum([accum.alertsCandidateCount, item.alertsCandidateCount]),
createdSignalsCount: accum.createdSignalsCount + item.createdSignalsCount,
createdSignals: [...accum.createdSignals, ...item.createdSignals],
warningMessages: [...accum.warningMessages, ...item.warningMessages],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ export const mlExecutor = async ({
isLoggedRequestsEnabled,
});
anomalyResults = searchResults.anomalyResults;
// Collect rule execution metrics
result.totalEventsFound = anomalyResults.hits.hits.length;
result.alertsCandidateCount = anomalyResults.hits.hits.length;
loggedRequests.push(...(searchResults.loggedRequests ?? []));
} catch (error) {
result.errors.push(error.message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { isObject, chunk } from 'lodash';
import { isObject, chunk, sum } from 'lodash';

import { NEW_TERMS_RULE_TYPE_ID } from '@kbn/securitysolution-rules';
import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
Expand Down Expand Up @@ -158,6 +158,7 @@ export const createNewTermsAlertType = (): SecurityAlertType<
result.warningMessages.push(exceptionsWarning);
}
let pageNumber = 0;
let alertsCandidateCount: number | undefined;

// There are 2 conditions that mean we're finished: either there were still too many alerts to create
// after deduplication and the array of alerts was truncated before being submitted to ES, or there were
Expand Down Expand Up @@ -413,6 +414,12 @@ export const createNewTermsAlertType = (): SecurityAlertType<
throw new Error('Aggregations were missing on document fetch search result');
}

// Collect rule execution metrics
alertsCandidateCount = sum([
alertsCandidateCount,
docFetchSearchResult.aggregations.new_terms.buckets.length,
]);

const bulkCreateResult = await createAlertsHook(docFetchSearchResult);

if (bulkCreateResult.alertsWereTruncated) {
Expand All @@ -435,7 +442,12 @@ export const createNewTermsAlertType = (): SecurityAlertType<
responseActions: completeRule.ruleParams.responseActions,
});

return { ...result, state, ...(isLoggedRequestsEnabled ? { loggedRequests } : {}) };
return {
...result,
state,
alertsCandidateCount,
...(isLoggedRequestsEnabled ? { loggedRequests } : {}),
};
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import type moment from 'moment';

import { sum } from 'lodash';
import type { estypes } from '@elastic/elasticsearch';

import { withSecuritySpan } from '../../../../../utils/with_security_span';
Expand Down Expand Up @@ -228,6 +228,9 @@ export const groupAndBulkCreate = async ({

const buckets = eventsByGroupResponseWithAggs.aggregations.eventGroups.buckets;

// Collect rule execution metrics
toReturn.alertsCandidateCount = sum(buckets.map((b) => b.doc_count));

// we can create only as many unsuppressed alerts, as total number of alerts(suppressed and unsuppressed) does not exceeds maxSignals
const maxUnsuppressedCount = tuple.maxSignals - buckets.length;
if (suppressOnMissingFields === false && maxUnsuppressedCount > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ export const thresholdExecutor = async ({
});
return {
...result,
alertsCandidateCount: buckets.length,
state: {
...state,
initialized: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export interface SecurityAlertTypeReturnValue<TState extends RuleTypeState> {
success: boolean;
warning: boolean;
warningMessages: string[];
alertsCandidateCount?: number;
suppressedAlertsCount?: number;
totalEventsFound?: number;
loggedRequests?: RulePreviewLoggedRequest[];
Expand Down Expand Up @@ -337,12 +338,16 @@ export interface SearchAfterAndBulkCreateReturnType {
searchAfterTimes: string[];
enrichmentTimes: string[];
bulkCreateTimes: string[];
/**
* The number of detected alerts. Suppression hasn't been applied yet.
*/
alertsCandidateCount?: number;
suppressedAlertsCount?: number;
createdSignalsCount: number;
createdSignals: unknown[];
errors: string[];
userError?: boolean;
warningMessages: string[];
suppressedAlertsCount?: number;
totalEventsFound?: number;
loggedRequests?: RulePreviewLoggedRequest[];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,7 @@ describe('utils', () => {
const expected: SearchAfterAndBulkCreateReturnType = {
bulkCreateTimes: [],
enrichmentTimes: [],
alertsCandidateCount: 0,
createdSignalsCount: 0,
createdSignals: [],
errors: [],
Expand All @@ -888,6 +889,7 @@ describe('utils', () => {
const expected: SearchAfterAndBulkCreateReturnType = {
bulkCreateTimes: [],
enrichmentTimes: [],
alertsCandidateCount: 1,
createdSignalsCount: 0,
createdSignals: [],
errors: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import agent from 'elastic-apm-node';
import { createHash } from 'crypto';
import { get, invert, isArray, isEmpty, merge } from 'lodash';
import { get, invert, isArray, isEmpty, merge, sum } from 'lodash';
import moment from 'moment';
import objectHash from 'object-hash';

Expand Down Expand Up @@ -627,6 +627,7 @@ export const createSearchAfterReturnTypeFromResponse = <
)
);
}),
alertsCandidateCount: searchResult.hits.hits.length,
});
};

Expand All @@ -636,6 +637,7 @@ export const createSearchAfterReturnType = ({
searchAfterTimes,
enrichmentTimes,
bulkCreateTimes,
alertsCandidateCount,
createdSignalsCount,
createdSignals,
errors,
Expand All @@ -648,6 +650,7 @@ export const createSearchAfterReturnType = ({
searchAfterTimes?: string[] | undefined;
enrichmentTimes?: string[] | undefined;
bulkCreateTimes?: string[] | undefined;
alertsCandidateCount?: number | undefined;
createdSignalsCount?: number | undefined;
createdSignals?: unknown[] | undefined;
errors?: string[] | undefined;
Expand All @@ -661,6 +664,7 @@ export const createSearchAfterReturnType = ({
searchAfterTimes: searchAfterTimes ?? [],
enrichmentTimes: enrichmentTimes ?? [],
bulkCreateTimes: bulkCreateTimes ?? [],
alertsCandidateCount,
createdSignalsCount: createdSignalsCount ?? 0,
createdSignals: createdSignals ?? [],
errors: errors ?? [],
Expand Down Expand Up @@ -701,6 +705,7 @@ export const mergeReturns = (
searchAfterTimes: existingSearchAfterTimes,
bulkCreateTimes: existingBulkCreateTimes,
enrichmentTimes: existingEnrichmentTimes,
alertsCandidateCount: existingAlertsCandidateCount,
createdSignalsCount: existingCreatedSignalsCount,
createdSignals: existingCreatedSignals,
errors: existingErrors,
Expand All @@ -715,6 +720,7 @@ export const mergeReturns = (
searchAfterTimes: newSearchAfterTimes,
enrichmentTimes: newEnrichmentTimes,
bulkCreateTimes: newBulkCreateTimes,
alertsCandidateCount: newAlertsCandidateCount,
createdSignalsCount: newCreatedSignalsCount,
createdSignals: newCreatedSignals,
errors: newErrors,
Expand All @@ -729,6 +735,7 @@ export const mergeReturns = (
searchAfterTimes: [...existingSearchAfterTimes, ...newSearchAfterTimes],
enrichmentTimes: [...existingEnrichmentTimes, ...newEnrichmentTimes],
bulkCreateTimes: [...existingBulkCreateTimes, ...newBulkCreateTimes],
alertsCandidateCount: sum([existingAlertsCandidateCount, newAlertsCandidateCount]),
createdSignalsCount: existingCreatedSignalsCount + newCreatedSignalsCount,
createdSignals: [...existingCreatedSignals, ...newCreatedSignals],
errors: [...new Set([...existingErrors, ...newErrors])],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* 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 expect from 'expect';
import { createRule, deleteAllRules, deleteAllAlerts } from '@kbn/detections-response-ftr-services';
import {
getEqlRuleParams,
getOpenAlerts,
dataGeneratorFactory,
getLatestSecurityRuleExecutionMetricsFromEventLog,
} from '../../../../utils';
import type { FtrProviderContext } from '../../../../../../ftr_provider_context';

export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const es = getService('es');
const log = getService('log');
const { indexListOfDocuments: indexListOfSourceDocuments } = dataGeneratorFactory({
es,
index: 'logs-1',
log,
});

describe('@ess @serverless Rule execution metrics for EQL rules', () => {
beforeEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);

await es.indices.delete({
index: 'logs-1',
ignore_unavailable: true,
});
await es.indices.create({
index: 'logs-1',
mappings: {
properties: {
'@timestamp': {
type: 'date',
},
host: {
properties: {
name: {
type: 'keyword',
},
},
},
},
},
});
});

describe('metrics collection', () => {
describe('alerts_candidate_count', () => {
it('records alerts_candidate_count value', async () => {
const timestamp = new Date().toISOString();
const document = {
'@timestamp': timestamp,
host: {
name: 'test',
},
};
const rule = getEqlRuleParams({
index: ['logs-1'],
query: 'any where true',
from: 'now-35m',
interval: '30m',
enabled: true,
});

await indexListOfSourceDocuments([document]);

const createdRule = await createRule(supertest, log, rule);
const alerts = await getOpenAlerts(supertest, log, es, createdRule);

expect(alerts.hits.hits).toHaveLength(1);

const { alerts_candidate_count } =
await getLatestSecurityRuleExecutionMetricsFromEventLog(es, log, createdRule.id);

expect(alerts_candidate_count).toBe(1);
});

it('records alerts_candidate_count higher than the number of suppressed alerts', async () => {
const timestamp = new Date().toISOString();
const document = {
'@timestamp': timestamp,
host: {
name: 'host-candidate-suppression-metrics',
},
};
const rule = getEqlRuleParams({
index: ['logs-1'],
query: 'any where true',
alert_suppression: {
group_by: ['host.name'],
duration: {
value: 300,
unit: 'm',
},
missing_fields_strategy: 'suppress',
},
from: 'now-35m',
interval: '30m',
enabled: true,
});

await indexListOfSourceDocuments([document, document]);

const createdRule = await createRule(supertest, log, rule);
const alerts = await getOpenAlerts(supertest, log, es, createdRule);

expect(alerts.hits.hits).toHaveLength(1);

const { alerts_candidate_count } =
await getLatestSecurityRuleExecutionMetricsFromEventLog(es, log, createdRule.id);

expect(alerts_candidate_count).toBe(2);
});
});
});
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
describe('EQL execution logic API', function () {
loadTestFile(require.resolve('./eql'));
loadTestFile(require.resolve('./eql_alert_suppression'));
loadTestFile(require.resolve('./eql_metrics'));
});
}
Loading
Loading