Skip to content

Commit e42630d

Browse files
authored
[Security Solution] [DETECTIONS] Set rule status to failure only on large gaps (#71549)
* only display gap error when a gap is too large for the gap mitigation code to cover, general code cleanup, adds some tests for separate function * removes throwing of errors and log error and return null for maxCatchup, ratio, and gapDiffInUnits properties * forgot to delete commented out code * remove math.abs since we fixed this bug by switching around logic when calculating gapDiffInUnits in getGapMaxCatchupRatio fn * updates tests for when a gap error should be written to rule status * fix typo
1 parent 0e7c3c7 commit e42630d

File tree

5 files changed

+258
-84
lines changed

5 files changed

+258
-84
lines changed

x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import { getResult, getMlResult } from '../routes/__mocks__/request_responses';
1010
import { signalRulesAlertType } from './signal_rule_alert_type';
1111
import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks';
1212
import { ruleStatusServiceFactory } from './rule_status_service';
13-
import { getGapBetweenRuns, getListsClient, getExceptions, sortExceptionItems } from './utils';
13+
import {
14+
getGapBetweenRuns,
15+
getGapMaxCatchupRatio,
16+
getListsClient,
17+
getExceptions,
18+
sortExceptionItems,
19+
} from './utils';
1420
import { RuleExecutorOptions } from './types';
1521
import { searchAfterAndBulkCreate } from './search_after_bulk_create';
1622
import { scheduleNotificationActions } from '../notifications/schedule_notification_actions';
@@ -97,6 +103,7 @@ describe('rules_notification_alert_type', () => {
97103
exceptionsWithValueLists: [],
98104
});
99105
(searchAfterAndBulkCreate as jest.Mock).mockClear();
106+
(getGapMaxCatchupRatio as jest.Mock).mockClear();
100107
(searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({
101108
success: true,
102109
searchAfterTimes: [],
@@ -126,22 +133,39 @@ describe('rules_notification_alert_type', () => {
126133
});
127134

128135
describe('executor', () => {
129-
it('should warn about the gap between runs', async () => {
130-
(getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(1000));
136+
it('should warn about the gap between runs if gap is very large', async () => {
137+
(getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(100, 'm'));
138+
(getGapMaxCatchupRatio as jest.Mock).mockReturnValue({
139+
maxCatchup: 4,
140+
ratio: 20,
141+
gapDiffInUnits: 95,
142+
});
131143
await alert.executor(payload);
132144
expect(logger.warn).toHaveBeenCalled();
133145
expect(logger.warn.mock.calls[0][0]).toContain(
134-
'a few seconds (1000ms) has passed since last rule execution, and signals may have been missed.'
146+
'2 hours (6000000ms) has passed since last rule execution, and signals may have been missed.'
135147
);
136148
expect(ruleStatusService.error).toHaveBeenCalled();
137149
expect(ruleStatusService.error.mock.calls[0][0]).toContain(
138-
'a few seconds (1000ms) has passed since last rule execution, and signals may have been missed.'
150+
'2 hours (6000000ms) has passed since last rule execution, and signals may have been missed.'
139151
);
140152
expect(ruleStatusService.error.mock.calls[0][1]).toEqual({
141-
gap: 'a few seconds',
153+
gap: '2 hours',
142154
});
143155
});
144156

157+
it('should NOT warn about the gap between runs if gap small', async () => {
158+
(getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(1, 'm'));
159+
(getGapMaxCatchupRatio as jest.Mock).mockReturnValue({
160+
maxCatchup: 1,
161+
ratio: 1,
162+
gapDiffInUnits: 1,
163+
});
164+
await alert.executor(payload);
165+
expect(logger.warn).toHaveBeenCalledTimes(0);
166+
expect(ruleStatusService.error).toHaveBeenCalledTimes(0);
167+
});
168+
145169
it("should set refresh to 'wait_for' when actions are present", async () => {
146170
const ruleAlert = getResult();
147171
ruleAlert.actions = [

x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ import {
2222
} from './search_after_bulk_create';
2323
import { getFilter } from './get_filter';
2424
import { SignalRuleAlertTypeDefinition, RuleAlertAttributes } from './types';
25-
import { getGapBetweenRuns, parseScheduleDates, getListsClient, getExceptions } from './utils';
25+
import {
26+
getGapBetweenRuns,
27+
parseScheduleDates,
28+
getListsClient,
29+
getExceptions,
30+
getGapMaxCatchupRatio,
31+
MAX_RULE_GAP_RATIO,
32+
} from './utils';
2633
import { signalParamsSchema } from './signal_params_schema';
2734
import { siemRuleActionGroups } from './siem_rule_action_groups';
2835
import { findMlSignals } from './find_ml_signals';
@@ -130,15 +137,26 @@ export const signalRulesAlertType = ({
130137

131138
const gap = getGapBetweenRuns({ previousStartedAt, interval, from, to });
132139
if (gap != null && gap.asMilliseconds() > 0) {
133-
const gapString = gap.humanize();
134-
const gapMessage = buildRuleMessage(
135-
`${gapString} (${gap.asMilliseconds()}ms) has passed since last rule execution, and signals may have been missed.`,
136-
'Consider increasing your look behind time or adding more Kibana instances.'
137-
);
138-
logger.warn(gapMessage);
140+
const fromUnit = from[from.length - 1];
141+
const { ratio } = getGapMaxCatchupRatio({
142+
logger,
143+
buildRuleMessage,
144+
previousStartedAt,
145+
ruleParamsFrom: from,
146+
interval,
147+
unit: fromUnit,
148+
});
149+
if (ratio && ratio >= MAX_RULE_GAP_RATIO) {
150+
const gapString = gap.humanize();
151+
const gapMessage = buildRuleMessage(
152+
`${gapString} (${gap.asMilliseconds()}ms) has passed since last rule execution, and signals may have been missed.`,
153+
'Consider increasing your look behind time or adding more Kibana instances.'
154+
);
155+
logger.warn(gapMessage);
139156

140-
hasError = true;
141-
await ruleStatusService.error(gapMessage, { gap: gapString });
157+
hasError = true;
158+
await ruleStatusService.error(gapMessage, { gap: gapString });
159+
}
142160
}
143161
try {
144162
const { listClient, exceptionsClient } = await getListsClient({

x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import { RuleAlertAction } from '../../../../common/detection_engine/types';
1111
import { RuleTypeParams } from '../types';
1212
import { SearchResponse } from '../../types';
1313

14+
// used for gap detection code
15+
export type unitType = 's' | 'm' | 'h';
16+
export const isValidUnit = (unitParam: string): unitParam is unitType =>
17+
['s', 'm', 'h'].includes(unitParam);
18+
1419
export interface SignalsParams {
1520
signalIds: string[] | undefined | null;
1621
query: object | undefined | null;

x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
parseScheduleDates,
2222
getDriftTolerance,
2323
getGapBetweenRuns,
24+
getGapMaxCatchupRatio,
2425
errorAggregator,
2526
getListsClient,
2627
hasLargeValueList,
@@ -716,6 +717,52 @@ describe('utils', () => {
716717
});
717718
});
718719

720+
describe('getMaxCatchupRatio', () => {
721+
test('should return null if rule has never run before', () => {
722+
const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({
723+
logger: mockLogger,
724+
previousStartedAt: null,
725+
interval: '30s',
726+
ruleParamsFrom: 'now-30s',
727+
buildRuleMessage,
728+
unit: 's',
729+
});
730+
expect(maxCatchup).toBeNull();
731+
expect(ratio).toBeNull();
732+
expect(gapDiffInUnits).toBeNull();
733+
});
734+
735+
test('should should have non-null values when gap is present', () => {
736+
const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({
737+
logger: mockLogger,
738+
previousStartedAt: moment().subtract(65, 's').toDate(),
739+
interval: '50s',
740+
ruleParamsFrom: 'now-55s',
741+
buildRuleMessage,
742+
unit: 's',
743+
});
744+
expect(maxCatchup).toEqual(0.2);
745+
expect(ratio).toEqual(0.2);
746+
expect(gapDiffInUnits).toEqual(10);
747+
});
748+
749+
// when a rule runs sooner than expected we don't
750+
// consider that a gap as that is a very rare circumstance
751+
test('should return null when given a negative gap (rule ran sooner than expected)', () => {
752+
const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({
753+
logger: mockLogger,
754+
previousStartedAt: moment().subtract(-15, 's').toDate(),
755+
interval: '10s',
756+
ruleParamsFrom: 'now-13s',
757+
buildRuleMessage,
758+
unit: 's',
759+
});
760+
expect(maxCatchup).toBeNull();
761+
expect(ratio).toBeNull();
762+
expect(gapDiffInUnits).toBeNull();
763+
});
764+
});
765+
719766
describe('#getExceptions', () => {
720767
test('it successfully returns array of exception list items', async () => {
721768
const client = listMock.getExceptionListClient();

0 commit comments

Comments
 (0)