-
Notifications
You must be signed in to change notification settings - Fork 8.6k
[Security Solution][Detections] Proposal: building Rule Execution Log on top of Event Log and ECS #94143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Security Solution][Detections] Proposal: building Rule Execution Log on top of Event Log and ECS #94143
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| "data", | ||
| "dataEnhanced", | ||
| "embeddable", | ||
| "eventLog", | ||
| "features", | ||
| "taskManager", | ||
| "inspector", | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,49 @@ | ||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||
| * 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 { IEvent as IEventLogEvent } from '../../../../../../event_log/server'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // https://www.elastic.co/guide/en/ecs/1.9/ecs-guidelines.html | ||||||||||||||||||||||||
| // https://www.elastic.co/guide/en/ecs/1.9/ecs-category-field-values-reference.html | ||||||||||||||||||||||||
| // https://www.elastic.co/guide/en/ecs/1.9/ecs-field-reference.html | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| export type IEcsEvent = IEventLogEvent & IEcsAdditionalFields; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| interface IEcsAdditionalFields { | ||||||||||||||||||||||||
| // https://www.elastic.co/guide/en/ecs/1.9/ecs-event.html | ||||||||||||||||||||||||
| event?: { | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already use a bunch of the kibana/x-pack/plugins/event_log/generated/schemas.ts Lines 38 to 48 in 6264c56
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So,
Not sure if I addressed your comment... Pls let me know :) |
||||||||||||||||||||||||
| dataset?: string; | ||||||||||||||||||||||||
| created?: string; | ||||||||||||||||||||||||
| kind?: string; | ||||||||||||||||||||||||
| type?: string[]; | ||||||||||||||||||||||||
| severity?: number; | ||||||||||||||||||||||||
| sequence?: number; | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // https://www.elastic.co/guide/en/ecs/1.9/ecs-log.html | ||||||||||||||||||||||||
| log?: { | ||||||||||||||||||||||||
| logger?: string; | ||||||||||||||||||||||||
| level?: string; | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // https://www.elastic.co/guide/en/ecs/1.9/ecs-rule.html | ||||||||||||||||||||||||
| rule?: { | ||||||||||||||||||||||||
| id?: string; | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // custom fields | ||||||||||||||||||||||||
| kibana?: { | ||||||||||||||||||||||||
| detection_engine?: { | ||||||||||||||||||||||||
| rule_status?: string; | ||||||||||||||||||||||||
| rule_status_severity?: number; | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| export type EcsEventKey = keyof IEcsEvent; | ||||||||||||||||||||||||
| export type EcsEventBaseKey = '@timestamp' | 'message' | 'tags'; | ||||||||||||||||||||||||
| export type EcsEventObjectKey = Exclude<EcsEventKey, EcsEventBaseKey>; | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| /* | ||
| * 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 { EcsEventObjectKey, IEcsEvent } from './ecs_event'; | ||
| import { RuleExecutionEventLevel, getLevelSeverity } from './rule_execution_event_levels'; | ||
| import { RuleExecutionStatus, getStatusSeverity } from './rule_execution_statuses'; | ||
|
|
||
| const EVENT_LOG_PROVIDER = 'detection-engine'; // TODO: "siem", "siem-detection-engine", "security-solution", other? | ||
| const EVENT_LOG_NAME = 'rule-execution-log'; // TODO: A more generic rule-log? A separate rule-management (rule-audit) log? | ||
|
|
||
| export class EcsEventBuilder { | ||
| private _result: IEcsEvent = {}; | ||
|
|
||
| constructor() { | ||
| // TODO: Which version does event_log use? Should it be specified here or inside the event log itself? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The event log plugin provides this value, you don't need to provide it. And probably shouldn't since we could end up with different versions in different documents. Though I don't know of anything that makes use of this field right now. |
||
| this.ecs('1.9.0'); | ||
| this.logger(EVENT_LOG_PROVIDER, EVENT_LOG_NAME); | ||
| } | ||
|
|
||
| /** | ||
| * Sets "@timestamp", message. | ||
| * https://www.elastic.co/guide/en/ecs/1.9/ecs-base.html | ||
| * @param eventDate When the event happened (not captured or created). Example: new Date(). | ||
| * @param eventMessage Example: "Machine learning job is not started". | ||
| */ | ||
| public baseFields(eventDate: Date, eventMessage: string): EcsEventBuilder { | ||
| return this.base({ | ||
| '@timestamp': eventDate.toISOString(), | ||
| message: eventMessage, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Sets event.provider, event.dataset, log.logger. | ||
| * https://www.elastic.co/guide/en/ecs/1.9/ecs-event.html | ||
| * https://www.elastic.co/guide/en/ecs/1.9/ecs-log.html | ||
| * @param logProvider 1st-level category (plugin, subsystem). Example: "detection-engine". | ||
| * @param logName 2nd-level category (feature, module). Example: "rule-execution-log". | ||
| */ | ||
| public logger(logProvider: string, logName: string): EcsEventBuilder { | ||
| return this.nested('event', { | ||
| provider: logProvider, | ||
| dataset: `${logProvider}.${logName}`, | ||
| }).nested('log', { | ||
| logger: `${logProvider}.${logName}`, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Sets log.level, event.severity. | ||
| * https://www.elastic.co/guide/en/ecs/1.9/ecs-event.html | ||
| * https://www.elastic.co/guide/en/ecs/1.9/ecs-log.html | ||
| * @param eventLevel Mapped to log.level. Example: "info", "error". | ||
| */ | ||
| public level(eventLevel: RuleExecutionEventLevel): EcsEventBuilder { | ||
| return this.nested('event', { | ||
| severity: getLevelSeverity(eventLevel), | ||
| }).nested('log', { | ||
| level: eventLevel, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Sets categorization fields: event.kind, event.type, event.action. | ||
| * https://www.elastic.co/guide/en/ecs/1.9/ecs-category-field-values-reference.html | ||
| * https://www.elastic.co/guide/en/ecs/1.9/ecs-event.html | ||
| * @param eventAction Actual event type. Example: "status-changed". | ||
| */ | ||
| public typeChange(eventAction: string): EcsEventBuilder { | ||
| return this.nested('event', { | ||
| kind: 'event', | ||
| type: ['change'], | ||
| action: eventAction, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Sets categorization fields: event.kind, event.type, event.action. | ||
| * https://www.elastic.co/guide/en/ecs/1.9/ecs-category-field-values-reference.html | ||
| * https://www.elastic.co/guide/en/ecs/1.9/ecs-event.html | ||
| * @param eventAction Actual event type. Example: "metric-search-duration-max", "metric-indexing-lookback". | ||
| */ | ||
| public typeMetric(eventAction: string): EcsEventBuilder { | ||
| return this.nested('event', { | ||
| kind: 'metric', | ||
| type: ['info'], | ||
| action: eventAction, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Sets any of the event.* fields. | ||
| * https://www.elastic.co/guide/en/ecs/1.9/ecs-event.html | ||
| */ | ||
| public event(fields: NonNullable<IEcsEvent['event']>): EcsEventBuilder { | ||
| return this.nested('event', fields); | ||
| } | ||
|
|
||
| /** | ||
| * https://www.elastic.co/guide/en/ecs/1.9/ecs-rule.html | ||
| * @param ruleId Dynamic rule id (alert id in the Alerting framework terminology). | ||
| * @param spaceId Kibana space id. | ||
| */ | ||
| public rule(ruleId: string, spaceId?: string): EcsEventBuilder { | ||
| const existingSavedObjectRefs = this._result.kibana?.saved_objects ?? []; | ||
| const newSavedObjectRefs = existingSavedObjectRefs.concat({ | ||
| type: 'alert', | ||
| id: ruleId, | ||
| namespace: spaceId, | ||
| }); | ||
|
|
||
| return this.nested('rule', { | ||
| id: ruleId, // TODO: "id" or "uuid"? | ||
| }).nested('kibana', { | ||
| saved_objects: newSavedObjectRefs, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Sets custom fields representing rule execution status: | ||
| * kibana.detection_engine.{rule_status, rule_status_severity} | ||
| * @param status Execution status of the rule. | ||
| */ | ||
| public ruleStatus(status: RuleExecutionStatus): EcsEventBuilder { | ||
| return this.nested('kibana', { | ||
| detection_engine: { | ||
| rule_status: status, | ||
| rule_status_severity: getStatusSeverity(status), | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Sets ecs.version. | ||
| * https://www.elastic.co/guide/en/ecs/1.9/ecs-ecs.html | ||
| * @param version Example: 1.7.0 | ||
| */ | ||
| public ecs(version: string): EcsEventBuilder { | ||
| return this.nested('ecs', { | ||
| version, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Builds and returns the final ECS event. | ||
| */ | ||
| public build(): IEcsEvent { | ||
| this.event({ | ||
| created: new Date().toISOString(), // TODO: del or use eventDate? | ||
| }); | ||
| return this._result; | ||
| } | ||
|
|
||
| private base(fields: IEcsEvent): EcsEventBuilder { | ||
| this._result = { ...this._result, ...fields }; | ||
| return this; | ||
| } | ||
|
|
||
| private nested<K extends EcsEventObjectKey, V extends IEcsEvent[K]>( | ||
| key: K, | ||
| fields: V | ||
| ): EcsEventBuilder { | ||
| this._result[key] = { | ||
| ...this._result[key], | ||
| ...fields, | ||
| }; | ||
| return this; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| /* | ||
| * 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. | ||
| */ | ||
|
|
||
| export * from './ecs_event'; | ||
| export * from './ecs_event_builder'; | ||
| export * from './rule_execution_event_levels'; | ||
| export * from './rule_execution_statuses'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| /* | ||
| * 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. | ||
| */ | ||
|
|
||
| // ----------------------------------------------------------------------------- | ||
| // Levels | ||
|
|
||
| export const RuleExecutionEventLevel = { | ||
| INFO: 'info', | ||
| WARNING: 'warning', | ||
| ERROR: 'error', | ||
| } as const; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm assuming part of this refactor is to also write our log statements to the event log? Is that correct?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a great question. TL;DR: I think we will be able to write both events to the execution log and normal logs. Normal logs are for Kibana sysadmins and debugging, execution logs are for Detections users. What to log where - I'd say TBD, but would be nice to have some flexibility here. You mean whatever we log through a Kibana logger to a normal log? I didn't imagine it exactly like that, because I'm not sure if it makes sense to see all the logs in the UI from the user perspective: can be too many, too technical or include info that should not be shown to the user. On the other hand, we definitely would be able to log more data, so I added these standard log levels because it's a UI thing familiar to all users. In Kibana we already have a few views where the user can scroll through logs, e.g. in Fleet there are logs from agents. |
||
|
|
||
| export type RuleExecutionEventLevel = typeof RuleExecutionEventLevel[keyof typeof RuleExecutionEventLevel]; | ||
|
|
||
| // ----------------------------------------------------------------------------- | ||
| // Level severities | ||
|
|
||
| type LevelMappingTo<TValue> = Readonly<Record<RuleExecutionEventLevel, TValue>>; | ||
|
|
||
| const levelSeverityByLevel: LevelMappingTo<number> = Object.freeze({ | ||
| [RuleExecutionEventLevel.INFO]: 10, | ||
| [RuleExecutionEventLevel.WARNING]: 20, | ||
| [RuleExecutionEventLevel.ERROR]: 30, | ||
| }); | ||
|
|
||
| export const getLevelSeverity = (level: RuleExecutionEventLevel): number => { | ||
| return levelSeverityByLevel[level] ?? 0; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| /* | ||
| * 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 { JobStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; | ||
|
|
||
| export type RuleExecutionStatus = JobStatus; | ||
|
|
||
| type StatusMappingTo<TValue> = Readonly<Record<RuleExecutionStatus, TValue>>; | ||
|
|
||
| const statusSeverityByStatus: StatusMappingTo<number> = Object.freeze({ | ||
| succeeded: 0, | ||
| 'going to run': 10, | ||
| warning: 20, | ||
| 'partial failure': 20, | ||
| failed: 30, | ||
| }); | ||
|
|
||
| export const getStatusSeverity = (status: RuleExecutionStatus): number => { | ||
| return statusSeverityByStatus[status] ?? 0; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think these will actually be written out today, till they're added to our schema - we use
dynamic: falseat the top of the mappings, so I believe they will not get indexed, at the very least.kibana/x-pack/plugins/event_log/generated/mappings.json
Lines 1 to 3 in 6264c56
If this PR is just an experiment, you might want to extend the schema, following the directions here: https://github.com/elastic/kibana/blob/master/x-pack/plugins/event_log/generated/README.md - and let us know if any of it is confusing so we can correct it.
I think we'd want to update the event log separately though, if we intend to merge this, just to keep the PRs a little cleaner.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this PR is just an experiment, a proposal. #94143 (comment)
I'll try to explain it better in a separate comment below :)