[Event Log] Populated rule.* ECS fields for alert events. (#101132)

* [Event Log] Populated rule.* ECS fields for alert events.

* added mappings

* changed the params passing

* fixed tests

* fixed type checks

* used kibanaVersion for version event rule

* fixed typos

* fixed tests

* fixed tests

* fixed tests

* fixed tests

* fixed jest tests

* removed references

* removed not populated fields

* fixed tests

* fixed tests

* fixed tests
This commit is contained in:
Yuliia Naumenko 2021-06-10 12:33:32 -07:00 committed by GitHub
parent befd30ff6c
commit e55a93ce58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 696 additions and 37 deletions

View file

@ -67,7 +67,7 @@ const createExecutionHandlerParams: jest.Mocked<
>
> = {
actionsPlugin: mockActionsPlugin,
spaceId: 'default',
spaceId: 'test1',
alertId: '1',
alertName: 'name-of-alert',
tags: ['tag-A', 'tag-B'],
@ -130,7 +130,7 @@ test('enqueues execution per selected action', async () => {
"apiKey": "MTIzOmFiYw==",
"id": "1",
"params": Object {
"alertVal": "My 1 name-of-alert default tag-A,tag-B 2 goes here",
"alertVal": "My 1 name-of-alert test1 tag-A,tag-B 2 goes here",
"contextVal": "My goes here",
"foo": true,
"stateVal": "My goes here",
@ -142,7 +142,7 @@ test('enqueues execution per selected action', async () => {
},
"type": "SAVED_OBJECT",
},
"spaceId": "default",
"spaceId": "test1",
},
]
`);
@ -154,6 +154,10 @@ test('enqueues execution per selected action', async () => {
Object {
"event": Object {
"action": "execute-action",
"category": Array [
"alerts",
],
"kind": "alert",
},
"kibana": Object {
"alerting": Object {
@ -164,18 +168,28 @@ test('enqueues execution per selected action', async () => {
"saved_objects": Array [
Object {
"id": "1",
"namespace": "test1",
"rel": "primary",
"type": "alert",
"type_id": "test",
},
Object {
"id": "1",
"namespace": "test1",
"type": "action",
"type_id": "test",
},
],
},
"message": "alert: test:1: 'name-of-alert' instanceId: '2' scheduled actionGroup: 'default' action: test:1",
"rule": Object {
"category": "test",
"id": "1",
"license": "basic",
"name": "name-of-alert",
"namespace": "test1",
"ruleset": "alerts",
},
},
],
]
@ -183,10 +197,10 @@ test('enqueues execution per selected action', async () => {
expect(jest.requireMock('./inject_action_params').injectActionParams).toHaveBeenCalledWith({
ruleId: '1',
spaceId: 'default',
spaceId: 'test1',
actionTypeId: 'test',
actionParams: {
alertVal: 'My 1 name-of-alert default tag-A,tag-B 2 goes here',
alertVal: 'My 1 name-of-alert test1 tag-A,tag-B 2 goes here',
contextVal: 'My goes here',
foo: true,
stateVal: 'My goes here',
@ -233,7 +247,7 @@ test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () =>
id: '1',
type: 'alert',
}),
spaceId: 'default',
spaceId: 'test1',
apiKey: createExecutionHandlerParams.apiKey,
});
});
@ -308,7 +322,7 @@ test('context attribute gets parameterized', async () => {
"apiKey": "MTIzOmFiYw==",
"id": "1",
"params": Object {
"alertVal": "My 1 name-of-alert default tag-A,tag-B 2 goes here",
"alertVal": "My 1 name-of-alert test1 tag-A,tag-B 2 goes here",
"contextVal": "My context-val goes here",
"foo": true,
"stateVal": "My goes here",
@ -320,7 +334,7 @@ test('context attribute gets parameterized', async () => {
},
"type": "SAVED_OBJECT",
},
"spaceId": "default",
"spaceId": "test1",
},
]
`);
@ -341,7 +355,7 @@ test('state attribute gets parameterized', async () => {
"apiKey": "MTIzOmFiYw==",
"id": "1",
"params": Object {
"alertVal": "My 1 name-of-alert default tag-A,tag-B 2 goes here",
"alertVal": "My 1 name-of-alert test1 tag-A,tag-B 2 goes here",
"contextVal": "My goes here",
"foo": true,
"stateVal": "My state-val goes here",
@ -353,7 +367,7 @@ test('state attribute gets parameterized', async () => {
},
"type": "SAVED_OBJECT",
},
"spaceId": "default",
"spaceId": "test1",
},
]
`);

View file

@ -174,7 +174,11 @@ export function createExecutionHandler<
const namespace = spaceId === 'default' ? {} : { namespace: spaceId };
const event: IEvent = {
event: { action: EVENT_LOG_ACTIONS.executeAction },
event: {
action: EVENT_LOG_ACTIONS.executeAction,
kind: 'alert',
category: [alertType.producer],
},
kibana: {
alerting: {
instance_id: alertInstanceId,
@ -192,6 +196,14 @@ export function createExecutionHandler<
{ type: 'action', id: action.id, type_id: action.actionTypeId, ...namespace },
],
},
rule: {
id: alertId,
license: alertType.minimumLicenseRequired,
category: alertType.id,
ruleset: alertType.producer,
...namespace,
name: alertName,
},
};
event.message = `alert: ${alertLabel} instanceId: '${alertInstanceId}' scheduled ${

View file

@ -303,6 +303,10 @@ export class TaskRunner<
event.message = `alert executed: ${alertLabel}`;
event.event = event.event || {};
event.event.outcome = 'success';
event.rule = {
...event.rule,
name: alert.name,
};
// Cleanup alert instances that are no longer scheduling actions to avoid over populating the alertInstances object
const instancesWithScheduledActions = pickBy(
@ -337,7 +341,8 @@ export class TaskRunner<
alertId,
alertLabel,
namespace,
ruleTypeId: alert.alertTypeId,
ruleType: alertType,
rule: alert,
});
if (!muteAll) {
@ -493,7 +498,11 @@ export class TaskRunner<
// explicitly set execute timestamp so it will be before other events
// generated here (new-instance, schedule-action, etc)
'@timestamp': runDate,
event: { action: EVENT_LOG_ACTIONS.execute },
event: {
action: EVENT_LOG_ACTIONS.execute,
kind: 'alert',
category: [this.alertType.producer],
},
kibana: {
saved_objects: [
{
@ -505,6 +514,13 @@ export class TaskRunner<
},
],
},
rule: {
id: alertId,
license: this.alertType.minimumLicenseRequired,
category: this.alertType.id,
ruleset: this.alertType.producer,
namespace,
},
};
eventLogger.startTiming(event);
@ -665,7 +681,19 @@ interface GenerateNewAndRecoveredInstanceEventsParams<
alertId: string;
alertLabel: string;
namespace: string | undefined;
ruleTypeId: string;
ruleType: NormalizedAlertType<
AlertTypeParams,
AlertTypeState,
{
[x: string]: unknown;
},
{
[x: string]: unknown;
},
string,
string
>;
rule: SanitizedAlert<AlertTypeParams>;
}
function generateNewAndRecoveredInstanceEvents<
@ -679,7 +707,8 @@ function generateNewAndRecoveredInstanceEvents<
currentAlertInstances,
originalAlertInstances,
recoveredAlertInstances,
ruleTypeId,
rule,
ruleType,
} = params;
const originalAlertInstanceIds = Object.keys(originalAlertInstances);
const currentAlertInstanceIds = Object.keys(currentAlertInstances);
@ -746,6 +775,8 @@ function generateNewAndRecoveredInstanceEvents<
const event: IEvent = {
event: {
action,
kind: 'alert',
category: [ruleType.producer],
...(state?.start ? { start: state.start as string } : {}),
...(state?.end ? { end: state.end as string } : {}),
...(state?.duration !== undefined ? { duration: state.duration as number } : {}),
@ -761,12 +792,20 @@ function generateNewAndRecoveredInstanceEvents<
rel: SAVED_OBJECT_REL_PRIMARY,
type: 'alert',
id: alertId,
type_id: ruleTypeId,
type_id: ruleType.id,
namespace,
},
],
},
message,
rule: {
id: rule.id,
license: ruleType.minimumLicenseRequired,
category: ruleType.id,
ruleset: ruleType.producer,
namespace,
name: rule.name,
},
};
eventLogger.logEvent(event);
}

View file

@ -101,11 +101,20 @@ Below is a document in the expected structure, with descriptions of the fields:
logger: "name of the logger",
},
// Rule fields. All of them are supported.
// Rule fields.
// https://www.elastic.co/guide/en/ecs/current/ecs-rule.html
rule: {
// Fields currently are populated:
id: "a823fd56-5467-4727-acb1-66809737d943", // rule id
category: "test", // rule type id
license: "basic", // rule type minimumLicenseRequired
name: "rule-name", //
ruleset: "alerts", // rule type producer
// Fields currently are not populated:
author: ["Elastic"],
id: "a823fd56-5467-4727-acb1-66809737d943",
description: "Some rule description",
version: '1',
uuid: "uuid"
// etc
},

View file

@ -214,6 +214,10 @@
"version": {
"ignore_above": 1024,
"type": "keyword"
},
"namespace": {
"ignore_above": 1024,
"type": "keyword"
}
}
},

View file

@ -91,6 +91,7 @@ export const EventSchema = schema.maybe(
ruleset: ecsString(),
uuid: ecsString(),
version: ecsString(),
namespace: ecsString(),
})
),
user: schema.maybe(

View file

@ -217,6 +217,7 @@ instanceStateValue: true
ruleTypeId: 'test.always-firing',
outcome: 'success',
message: `alert executed: test.always-firing:${alertId}: 'abc'`,
ruleObject: alertSearchResultWithoutDates,
});
break;
default:
@ -1249,10 +1250,11 @@ instanceStateValue: true
outcome: string;
message: string;
errorMessage?: string;
ruleObject: any;
}
async function validateEventLog(params: ValidateEventLogParams): Promise<void> {
const { spaceId, alertId, ruleTypeId, outcome, message, errorMessage } = params;
const { spaceId, alertId, outcome, message, errorMessage, ruleObject } = params;
const events: IValidatedEvent[] = await retry.try(async () => {
return await getEventLog({
@ -1293,10 +1295,19 @@ instanceStateValue: true
type: 'alert',
id: alertId,
namespace: spaceId,
type_id: ruleTypeId,
type_id: ruleObject.alertInfo.ruleTypeId,
},
]);
expect(event?.rule).to.eql({
id: alertId,
license: 'basic',
category: ruleObject.alertInfo.ruleTypeId,
ruleset: ruleObject.alertInfo.producer,
namespace: spaceId,
name: ruleObject.alertInfo.name,
});
expect(event?.message).to.eql(message);
if (errorMessage) {

View file

@ -81,6 +81,13 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
errorMessage: 'Unable to decrypt attribute "apiKey"',
status: 'error',
reason: 'decrypt',
rule: {
id: alertId,
category: response.body.rule_type_id,
license: 'basic',
ruleset: 'alertsFixture',
namespace: spaceId,
},
});
});
});

View file

@ -134,6 +134,14 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
outcome: 'success',
message: `alert executed: test.patternFiring:${alertId}: 'abc'`,
status: executeStatuses[executeCount++],
rule: {
id: alertId,
category: response.body.rule_type_id,
license: 'basic',
ruleset: 'alertsFixture',
namespace: Spaces.space1.id,
name: response.body.name,
},
});
break;
case 'execute-action':
@ -146,6 +154,14 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup: 'default' action: test.noop:${createdAction.id}`,
instanceId: 'instance',
actionGroupId: 'default',
rule: {
id: alertId,
category: response.body.rule_type_id,
license: 'basic',
ruleset: 'alertsFixture',
namespace: Spaces.space1.id,
name: response.body.name,
},
});
break;
case 'new-instance':
@ -181,6 +197,14 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
instanceId: 'instance',
actionGroupId: 'default',
shouldHaveEventEnd,
rule: {
id: alertId,
category: response.body.rule_type_id,
license: 'basic',
ruleset: 'alertsFixture',
namespace: Spaces.space1.id,
name: response.body.name,
},
});
}
});
@ -279,6 +303,14 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
outcome: 'success',
message: `alert executed: test.patternFiring:${alertId}: 'abc'`,
status: executeStatuses[executeCount++],
rule: {
id: alertId,
category: response.body.rule_type_id,
license: 'basic',
ruleset: 'alertsFixture',
namespace: Spaces.space1.id,
name: response.body.name,
},
});
break;
case 'execute-action':
@ -294,6 +326,14 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})' action: test.noop:${createdAction.id}`,
instanceId: 'instance',
actionGroupId: 'default',
rule: {
id: alertId,
category: response.body.rule_type_id,
license: 'basic',
ruleset: 'alertsFixture',
namespace: Spaces.space1.id,
name: response.body.name,
},
});
break;
case 'new-instance':
@ -332,6 +372,14 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
instanceId: 'instance',
actionGroupId: 'default',
shouldHaveEventEnd,
rule: {
id: alertId,
category: response.body.rule_type_id,
license: 'basic',
ruleset: 'alertsFixture',
namespace: Spaces.space1.id,
name: response.body.name,
},
});
}
});
@ -374,6 +422,13 @@ export default function eventLogTests({ getService }: FtrProviderContext) {
errorMessage: 'this alert is intended to fail',
status: 'error',
reason: 'execute',
rule: {
id: alertId,
category: response.body.rule_type_id,
license: 'basic',
ruleset: 'alertsFixture',
namespace: Spaces.space1.id,
},
});
});
});
@ -397,10 +452,21 @@ interface ValidateEventLogParams {
actionGroupId?: string;
instanceId?: string;
reason?: string;
rule: {
id: string;
name?: string;
version?: string;
category?: string;
reference?: string;
author?: string[];
license?: string;
ruleset?: string;
namespace?: string;
};
}
export function validateEvent(event: IValidatedEvent, params: ValidateEventLogParams): void {
const { spaceId, savedObjects, outcome, message, errorMessage } = params;
const { spaceId, savedObjects, outcome, message, errorMessage, rule } = params;
const { status, actionGroupId, instanceId, reason, shouldHaveEventEnd } = params;
if (status) {
@ -456,6 +522,8 @@ export function validateEvent(event: IValidatedEvent, params: ValidateEventLogPa
expect(event?.message).to.eql(message);
expect(event?.rule).to.eql(rule);
if (errorMessage) {
expect(event?.error?.message).to.eql(errorMessage);
}