Skip to content

Commit

Permalink
Add intended timestamp (elastic#191717)
Browse files Browse the repository at this point in the history
## Add new field to alert


Add optional `kibana.alert.intended_timestamp`. For scheduled rules it
has the same values as ALERT_RULE_EXECUTION_TIMESTAMP
(`kibana.alert.rule.execution.timestamp`)

for manual rule runs (backfill) it - will get the startedAtOverridden 

For example if i have event at 14:30

And if we run manual rule run from 14:00-15:00, then alert will have
`kibana.alert.intended_timestamp` at 15:00

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
nkhristinin and elasticmachine authored Sep 9, 2024
1 parent 9833f0f commit af399c1
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
EVENT_KIND,
EVENT_ORIGINAL,
TAGS,
ALERT_INTENDED_TIMESTAMP,
} from '@kbn/rule-data-utils';
import { MultiField } from './types';

Expand Down Expand Up @@ -133,6 +134,11 @@ export const alertFieldMap = {
array: false,
required: false,
},
[ALERT_INTENDED_TIMESTAMP]: {
type: 'date',
array: false,
required: false,
},
[ALERT_RULE_EXECUTION_UUID]: {
type: 'keyword',
array: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const AlertOptional = rt.partial({
'kibana.alert.end': schemaDate,
'kibana.alert.flapping': schemaBoolean,
'kibana.alert.flapping_history': schemaBooleanArray,
'kibana.alert.intended_timestamp': schemaDate,
'kibana.alert.last_detected': schemaDate,
'kibana.alert.maintenance_window_ids': schemaStringArray,
'kibana.alert.previous_action_group': schemaString,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ const SecurityAlertOptional = rt.partial({
'kibana.alert.group.id': schemaString,
'kibana.alert.group.index': schemaNumber,
'kibana.alert.host.criticality_level': schemaString,
'kibana.alert.intended_timestamp': schemaDate,
'kibana.alert.last_detected': schemaDate,
'kibana.alert.maintenance_window_ids': schemaStringArray,
'kibana.alert.new_terms': schemaStringArray,
Expand Down
5 changes: 5 additions & 0 deletions packages/kbn-rule-data-utils/src/default_alerts_as_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ const ALERT_INSTANCE_ID = `${ALERT_NAMESPACE}.instance.id` as const;
// kibana.alert.last_detected - timestamp when the alert was last seen
const ALERT_LAST_DETECTED = `${ALERT_NAMESPACE}.last_detected` as const;

// kiana.alert.intended_timestamp - timestamp when the alert was intended to be detected, useful for backfilling
const ALERT_INTENDED_TIMESTAMP = `${ALERT_NAMESPACE}.intended_timestamp` as const;

// kibana.alert.reason - human readable reason that this alert is active
const ALERT_REASON = `${ALERT_NAMESPACE}.reason` as const;

Expand Down Expand Up @@ -141,6 +144,7 @@ const fields = {
ALERT_RULE_CATEGORY,
ALERT_RULE_CONSUMER,
ALERT_RULE_EXECUTION_TIMESTAMP,
ALERT_INTENDED_TIMESTAMP,
ALERT_RULE_EXECUTION_UUID,
ALERT_RULE_NAME,
ALERT_RULE_PARAMETERS,
Expand Down Expand Up @@ -185,6 +189,7 @@ export {
ALERT_RULE_CATEGORY,
ALERT_RULE_CONSUMER,
ALERT_RULE_EXECUTION_TIMESTAMP,
ALERT_INTENDED_TIMESTAMP,
ALERT_RULE_EXECUTION_UUID,
ALERT_RULE_NAME,
ALERT_RULE_PARAMETERS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ describe('mappingFromFieldMap', () => {
},
},
},
intended_timestamp: {
type: 'date',
},
rule: {
properties: {
category: {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ it('matches snapshot', () => {
"required": true,
"type": "keyword",
},
"kibana.alert.intended_timestamp": Object {
"array": false,
"required": false,
"type": "date",
},
"kibana.alert.last_detected": Object {
"array": false,
"required": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
TIMESTAMP,
VERSION,
ALERT_RULE_EXECUTION_TIMESTAMP,
ALERT_INTENDED_TIMESTAMP,
} from '@kbn/rule-data-utils';
import { mapKeys, snakeCase } from 'lodash/fp';
import type { IRuleDataClient } from '..';
Expand Down Expand Up @@ -55,11 +56,13 @@ const augmentAlerts = <T>({
options,
kibanaVersion,
currentTimeOverride,
intendedTimestamp,
}: {
alerts: Array<{ _id: string; _source: T }>;
options: RuleExecutorOptions<any, any, any, any, any>;
kibanaVersion: string;
currentTimeOverride: Date | undefined;
intendedTimestamp: Date | undefined;
}) => {
const commonRuleFields = getCommonAlertFields(options);
return alerts.map((alert) => {
Expand All @@ -69,6 +72,9 @@ const augmentAlerts = <T>({
[ALERT_RULE_EXECUTION_TIMESTAMP]: new Date(),
[ALERT_START]: currentTimeOverride ?? new Date(),
[ALERT_LAST_DETECTED]: currentTimeOverride ?? new Date(),
[ALERT_INTENDED_TIMESTAMP]: intendedTimestamp
? intendedTimestamp
: currentTimeOverride ?? new Date(),
[VERSION]: kibanaVersion,
...(options?.maintenanceWindowIds?.length
? { [ALERT_MAINTENANCE_WINDOW_IDS]: options.maintenanceWindowIds }
Expand Down Expand Up @@ -251,6 +257,7 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper
...options.services,
alertWithPersistence: async (alerts, refresh, maxAlerts = undefined, enrichAlerts) => {
const numAlerts = alerts.length;

logger.debug(`Found ${numAlerts} alerts.`);

const ruleDataClientWriter = await ruleDataClient.getWriter({
Expand Down Expand Up @@ -297,11 +304,17 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper
alertsWereTruncated = true;
}

let intendedTimestamp;
if (options.startedAtOverridden) {
intendedTimestamp = options.startedAt;
}

const augmentedAlerts = augmentAlerts({
alerts: enrichedAlerts,
options,
kibanaVersion: ruleDataClient.kibanaVersion,
currentTimeOverride: undefined,
intendedTimestamp,
});

const response = await ruleDataClientWriter.bulk({
Expand Down Expand Up @@ -381,6 +394,11 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper

let alertsWereTruncated = false;

let intendedTimestamp;
if (options.startedAtOverridden) {
intendedTimestamp = options.startedAt;
}

if (writeAlerts && alerts.length > 0) {
const suppressionWindowStart = dateMath.parse(suppressionWindow, {
forceNow: currentTimeOverride,
Expand Down Expand Up @@ -560,6 +578,7 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper
options,
kibanaVersion: ruleDataClient.kibanaVersion,
currentTimeOverride,
intendedTimestamp,
});

const bulkResponse = await ruleDataClientWriter.bulk({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
ALERT_SUPPRESSION_TERMS,
TIMESTAMP,
ALERT_LAST_DETECTED,
ALERT_INTENDED_TIMESTAMP,
ALERT_RULE_EXECUTION_TIMESTAMP,
} from '@kbn/rule-data-utils';
import { flattenWithPrefix } from '@kbn/securitysolution-rules';
import { Rule } from '@kbn/alerting-plugin/common';
Expand Down Expand Up @@ -538,6 +540,19 @@ export default ({ getService }: FtrProviderContext) => {
expect(previewAlerts.length).toEqual(1);
});

it('should generate alerts with the correct intended timestamp fields', async () => {
const rule: QueryRuleCreateProps = {
...getRuleForAlertTesting(['auditbeat-*']),
query: `_id:${ID}`,
};

const { previewId } = await previewRule({ supertest, rule });
const previewAlerts = await getPreviewAlerts({ es, previewId });
const alert = previewAlerts[0]._source;

expect(alert?.[ALERT_INTENDED_TIMESTAMP]).toEqual(alert?.[TIMESTAMP]);
});

describe('with suppression enabled', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/security_solution/suppression');
Expand Down Expand Up @@ -2447,6 +2462,56 @@ export default ({ getService }: FtrProviderContext) => {
);
});

it('alerts has intended_timestamp set to the time of the manual run', async () => {
const id = uuidv4();
const firstTimestamp = moment(new Date()).subtract(3, 'h').toISOString();
const secondTimestamp = new Date().toISOString();
const firstDocument = {
id,
'@timestamp': firstTimestamp,
agent: {
name: 'agent-1',
},
};
const secondDocument = {
id,
'@timestamp': secondTimestamp,
agent: {
name: 'agent-2',
},
};
await indexListOfDocuments([firstDocument, secondDocument]);

const rule: QueryRuleCreateProps = {
...getRuleForAlertTesting(['ecs_compliant']),
rule_id: 'rule-1',
query: `id:${id}`,
from: 'now-1h',
interval: '1h',
};
const createdRule = await createRule(supertest, log, rule);
const alerts = await getAlerts(supertest, log, es, createdRule);

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

expect(alerts.hits.hits[0]?._source?.[ALERT_INTENDED_TIMESTAMP]).toEqual(
alerts.hits.hits[0]?._source?.[ALERT_RULE_EXECUTION_TIMESTAMP]
);

const backfillStartDate = moment(firstTimestamp).startOf('hour');
const backfillEndDate = moment(backfillStartDate).add(1, 'h');
const backfill = await scheduleRuleRun(supertest, [createdRule.id], {
startDate: backfillStartDate,
endDate: backfillEndDate,
});

await waitForBackfillExecuted(backfill, [createdRule.id], { supertest, log });
const allNewAlerts = await getAlerts(supertest, log, es, createdRule);
expect(allNewAlerts.hits.hits[1]?._source?.[ALERT_INTENDED_TIMESTAMP]).toEqual(
backfillEndDate.toISOString()
);
});

it('alerts when run on a time range that the rule has not previously seen, and deduplicates if run there more than once', async () => {
const id = uuidv4();
const firstTimestamp = moment(new Date()).subtract(3, 'h').toISOString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ function alertsAreTheSame(alertsA: any[], alertsB: any[]): void {
'kibana.alert.rule.uuid',
'kibana.alert.rule.execution.uuid',
'kibana.alert.rule.execution.timestamp',
'kibana.alert.intended_timestamp',
'kibana.alert.start',
'kibana.alert.reason',
'kibana.alert.uuid',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { DetectionAlert } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { ALERT_LAST_DETECTED, ALERT_START } from '@kbn/rule-data-utils';
import { ALERT_LAST_DETECTED, ALERT_START, ALERT_INTENDED_TIMESTAMP } from '@kbn/rule-data-utils';

export const removeRandomValuedPropertiesFromAlert = (alert: DetectionAlert | undefined) => {
if (!alert) {
Expand All @@ -24,6 +24,7 @@ export const removeRandomValuedPropertiesFromAlert = (alert: DetectionAlert | un
'kibana.alert.url': alertURL,
[ALERT_START]: alertStart,
[ALERT_LAST_DETECTED]: lastDetected,
[ALERT_INTENDED_TIMESTAMP]: intendedTimestamp,
...restOfAlert
} = alert;
return restOfAlert;
Expand Down

0 comments on commit af399c1

Please sign in to comment.