Skip to content
Closed
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
1 change: 1 addition & 0 deletions x-pack/plugins/security_solution/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"data",
"dataEnhanced",
"embeddable",
"eventLog",
"features",
"taskManager",
"inspector",
Expand Down
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 {
Copy link
Copy Markdown
Contributor

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: false at the top of the mappings, so I believe they will not get indexed, at the very least.

{
"dynamic": "false",
"properties": {

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.

Copy link
Copy Markdown
Contributor Author

@banderror banderror Mar 11, 2021

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 :)

// https://www.elastic.co/guide/en/ecs/1.9/ecs-event.html
event?: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already use a bunch of the event fields for fairly "generic" purposes, so I'm a little concerned about having application-specific data in here as well. Not a problem now since there's no overlap, but I'm guessing there could be some day?

event: schema.maybe(
schema.object({
action: ecsString(),
provider: ecsString(),
start: ecsDate(),
duration: ecsNumber(),
end: ecsDate(),
outcome: ecsString(),
reason: ecsString(),
})
),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, IEcsAdditionalFields just represents the fields we need to be supported in Event Log and which are currently missing in its schema. I mean, this PR is just a proposal, this interface is not going to be used in the working code as is and the missing fields will need to be moved to Event Log itself.

event fields here are all standard ECS fields (spec), and the only custom field I propose is kibana.detection_engine :)

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?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

@banderror banderror Mar 11, 2021

Choose a reason for hiding this comment

The 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;
};
Loading