mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution] Fixes threshold alert "Investigate in Timeline" functionality (#121256) (#122699)
* Add flattend parameters object and populate it in Security Solution
* Fix severity, risk_score, bugs, tests
* Add ALERT_RULE_PARAMETERS to package
* Skip tightly coupled test
* fix more tests
* Remove unused import
* Fix threat matching API test
* Continue overriding kibana.alert.rule.risk_score and severity for now
* Add ignore_above to ALERT_RULE_PARAMETERS
* Exploratory
* Not pretty
* more garbage
* debugging
* use expandDottedObject for alerts data in UI
* Remove kibana.alert.rule.risk_score and severity
* Fix tests related to risk_score and severity
* Make translation a template
* Can't use expression in template literal
* Remove commented line added by bad merge
* Fix linting
* Fix unflattening of UI data
* Fix mapping
* Remove console logs
* Fix imports
* Clean up, fix dupes
* Remaining test and type errors
* Remove comment
* Fix skip param
* Add backcompat for threshold timeline
* Fix linting
* Use indexNames for threshold timeline instead of data view
* Add tests for threshold timeline action
* Implement suggestion for simplified alertIds initialization
Co-authored-by: Marshall Main <marshall.main@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
(cherry picked from commit 27a9df79e7
)
# Conflicts:
# x-pack/plugins/security_solution/public/common/utils/alerts.ts
# x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
# x-pack/plugins/security_solution/public/helpers.tsx
# x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts
This commit is contained in:
parent
ecfd50b11b
commit
9e80a2f6c8
19 changed files with 435 additions and 74 deletions
|
@ -256,7 +256,7 @@ export interface SignalEcs {
|
|||
}
|
||||
|
||||
export type SignalEcsAAD = Exclude<SignalEcs, 'rule' | 'status'> & {
|
||||
rule?: Exclude<RuleEcs, 'id'> & { uuid: string[] };
|
||||
rule?: Exclude<RuleEcs, 'id'> & { parameters: Record<string, unknown>; uuid: string[] };
|
||||
building_block_type?: string[];
|
||||
workflow_status?: string[];
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@ export interface SignalEcs {
|
|||
}
|
||||
|
||||
export type SignalEcsAAD = Exclude<SignalEcs, 'rule' | 'status'> & {
|
||||
rule?: Exclude<RuleEcs, 'id'> & { uuid: string[] };
|
||||
rule?: Exclude<RuleEcs, 'id'> & { parameters: Record<string, unknown>; uuid: string[] };
|
||||
building_block_type?: string[];
|
||||
workflow_status?: string[];
|
||||
};
|
||||
|
|
|
@ -146,13 +146,18 @@ export const rulesFieldMap = {
|
|||
array: true,
|
||||
required: false,
|
||||
},
|
||||
'kibana.alert.rule.threshold.field': {
|
||||
type: 'keyword',
|
||||
'kibana.alert.rule.threshold': {
|
||||
type: 'object',
|
||||
array: true,
|
||||
required: false,
|
||||
},
|
||||
'kibana.alert.rule.threshold.field': {
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
required: false,
|
||||
},
|
||||
'kibana.alert.rule.threshold.value': {
|
||||
type: 'float', // TODO: should be 'long' (eventually, after we stabilize)
|
||||
type: 'float',
|
||||
array: false,
|
||||
required: false,
|
||||
},
|
||||
|
|
|
@ -11,6 +11,7 @@ export * from './hook_wrapper';
|
|||
export * from './index_pattern';
|
||||
export * from './mock_detail_item';
|
||||
export * from './mock_detection_alerts';
|
||||
export * from './mock_detection_alerts_aad';
|
||||
export * from './mock_ecs';
|
||||
export * from './mock_local_storage';
|
||||
export * from './mock_timeline_data';
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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 { Ecs } from '../../../common/ecs';
|
||||
|
||||
export const mockAADEcsDataWithAlert: Ecs = {
|
||||
_id: '1',
|
||||
timestamp: '2021-01-10T21:12:47.839Z',
|
||||
host: {
|
||||
name: ['apache'],
|
||||
ip: ['192.168.0.1'],
|
||||
},
|
||||
event: {
|
||||
id: ['1'],
|
||||
action: ['Action'],
|
||||
category: ['Access'],
|
||||
module: ['nginx'],
|
||||
severity: [3],
|
||||
},
|
||||
source: {
|
||||
ip: ['192.168.0.1'],
|
||||
port: [80],
|
||||
},
|
||||
destination: {
|
||||
ip: ['192.168.0.3'],
|
||||
port: [6343],
|
||||
},
|
||||
user: {
|
||||
id: ['1'],
|
||||
name: ['john.dee'],
|
||||
},
|
||||
geo: {
|
||||
region_name: ['xx'],
|
||||
country_iso_code: ['xx'],
|
||||
},
|
||||
kibana: {
|
||||
alert: {
|
||||
original_time: ['2021-01-10T21:12:45.839Z'],
|
||||
rule: {
|
||||
created_at: ['2021-01-10T21:12:47.839Z'],
|
||||
updated_at: ['2021-01-10T21:12:47.839Z'],
|
||||
created_by: ['elastic'],
|
||||
description: ['24/7'],
|
||||
enabled: [true],
|
||||
false_positives: ['test-1'],
|
||||
parameters: {
|
||||
filters: [],
|
||||
language: ['kuery'],
|
||||
query: ['user.id:1'],
|
||||
},
|
||||
from: ['now-300s'],
|
||||
uuid: ['b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'],
|
||||
immutable: [false],
|
||||
index: ['auditbeat-*'],
|
||||
interval: ['5m'],
|
||||
rule_id: ['rule-id-1'],
|
||||
output_index: [''],
|
||||
max_signals: [100],
|
||||
risk_score: ['21'],
|
||||
references: ['www.test.co'],
|
||||
saved_id: ["Garrett's IP"],
|
||||
timeline_id: ['1234-2136-11ea-9864-ebc8cc1cb8c2'],
|
||||
timeline_title: ['Untitled timeline'],
|
||||
severity: ['low'],
|
||||
updated_by: ['elastic'],
|
||||
tags: [],
|
||||
to: ['now'],
|
||||
type: ['saved_query'],
|
||||
threat: [],
|
||||
note: ['# this is some markdown documentation'],
|
||||
version: ['1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getDetectionAlertAADMock = (overrides: Partial<Ecs> = {}): Ecs => ({
|
||||
...mockAADEcsDataWithAlert,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const getThresholdDetectionAlertAADMock = (overrides: Partial<Ecs> = {}): Ecs[] => [
|
||||
{
|
||||
...mockAADEcsDataWithAlert,
|
||||
kibana: {
|
||||
alert: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert,
|
||||
rule: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert?.rule,
|
||||
parameters: {
|
||||
...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters,
|
||||
threshold: {
|
||||
field: ['destination.ip'],
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
name: ['mock threshold rule'],
|
||||
saved_id: [],
|
||||
type: ['threshold'],
|
||||
uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'],
|
||||
},
|
||||
threshold_result: {
|
||||
count: 99,
|
||||
from: '2021-01-10T21:11:45.839Z',
|
||||
cardinality: [
|
||||
{
|
||||
field: 'source.ip',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
terms: [
|
||||
{
|
||||
field: 'destination.ip',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
...overrides,
|
||||
},
|
||||
];
|
156
x-pack/plugins/security_solution/public/common/utils/alerts.ts
Normal file
156
x-pack/plugins/security_solution/public/common/utils/alerts.ts
Normal file
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* 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 { merge } from '@kbn/std';
|
||||
import { isPlainObject } from 'lodash';
|
||||
import { Ecs } from '../../../../cases/common';
|
||||
|
||||
// TODO we need to allow -> docValueFields: [{ field: "@timestamp" }],
|
||||
export const buildAlertsQuery = (alertIds: string[]) => {
|
||||
if (alertIds.length === 0) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
query: {
|
||||
bool: {
|
||||
filter: {
|
||||
ids: {
|
||||
values: alertIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 10000,
|
||||
};
|
||||
};
|
||||
|
||||
export const toStringArray = (value: unknown): string[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.reduce<string[]>((acc, v) => {
|
||||
if (v != null) {
|
||||
switch (typeof v) {
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
return [...acc, v.toString()];
|
||||
case 'object':
|
||||
try {
|
||||
return [...acc, JSON.stringify(v)];
|
||||
} catch {
|
||||
return [...acc, 'Invalid Object'];
|
||||
}
|
||||
case 'string':
|
||||
return [...acc, v];
|
||||
default:
|
||||
return [...acc, `${v}`];
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
} else if (value == null) {
|
||||
return [];
|
||||
} else if (!Array.isArray(value) && typeof value === 'object') {
|
||||
try {
|
||||
return [JSON.stringify(value)];
|
||||
} catch {
|
||||
return ['Invalid Object'];
|
||||
}
|
||||
} else {
|
||||
return [`${value}`];
|
||||
}
|
||||
};
|
||||
|
||||
const formatAlertItem = (item: unknown): Ecs => {
|
||||
if (item != null && isPlainObject(item)) {
|
||||
return Object.keys(item as object).reduce(
|
||||
(acc, key) => ({
|
||||
...acc,
|
||||
[key]: formatAlertItem((item as Record<string, unknown>)[key]),
|
||||
}),
|
||||
{} as Ecs
|
||||
);
|
||||
} else if (Array.isArray(item)) {
|
||||
return item.map((arrayItem): Ecs => formatAlertItem(arrayItem)) as unknown as Ecs;
|
||||
}
|
||||
return item as Ecs;
|
||||
};
|
||||
|
||||
const expandDottedField = (dottedFieldName: string, val: unknown): object => {
|
||||
const parts = dottedFieldName.split('.');
|
||||
if (parts.length === 1) {
|
||||
return { [parts[0]]: val };
|
||||
} else {
|
||||
return { [parts[0]]: expandDottedField(parts.slice(1).join('.'), val) };
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Expands an object with "dotted" fields to a nested object with unflattened fields.
|
||||
*
|
||||
* Example:
|
||||
* expandDottedObject({
|
||||
* "kibana.alert.depth": 1,
|
||||
* "kibana.alert.ancestors": [{
|
||||
* id: "d5e8eb51-a6a0-456d-8a15-4b79bfec3d71",
|
||||
* type: "event",
|
||||
* index: "signal_index",
|
||||
* depth: 0,
|
||||
* }],
|
||||
* })
|
||||
*
|
||||
* => {
|
||||
* kibana: {
|
||||
* alert: {
|
||||
* ancestors: [
|
||||
* id: "d5e8eb51-a6a0-456d-8a15-4b79bfec3d71",
|
||||
* type: "event",
|
||||
* index: "signal_index",
|
||||
* depth: 0,
|
||||
* ],
|
||||
* depth: 1,
|
||||
* },
|
||||
* },
|
||||
* }
|
||||
*/
|
||||
export const expandDottedObject = (dottedObj: object) => {
|
||||
if (Array.isArray(dottedObj)) {
|
||||
return dottedObj;
|
||||
}
|
||||
return Object.entries(dottedObj).reduce(
|
||||
(acc, [key, val]) => merge(acc, expandDottedField(key, val)),
|
||||
{}
|
||||
);
|
||||
};
|
||||
|
||||
export const formatAlertToEcsSignal = (alert: Record<string, unknown>): Ecs => {
|
||||
return expandDottedObject(alert) as Ecs;
|
||||
};
|
||||
|
||||
interface Signal {
|
||||
rule: {
|
||||
id: string;
|
||||
name: string;
|
||||
to: string;
|
||||
from: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SignalHit {
|
||||
_id: string;
|
||||
_index: string;
|
||||
_source: {
|
||||
'@timestamp': string;
|
||||
signal: Signal;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
_id: string;
|
||||
_index: string;
|
||||
'@timestamp': string;
|
||||
signal: Signal;
|
||||
[key: string]: unknown;
|
||||
}
|
|
@ -10,10 +10,11 @@ import moment from 'moment';
|
|||
|
||||
import { sendAlertToTimelineAction, determineToAndFrom } from './actions';
|
||||
import {
|
||||
mockEcsDataWithAlert,
|
||||
defaultTimelineProps,
|
||||
mockTimelineResult,
|
||||
getThresholdDetectionAlertAADMock,
|
||||
mockEcsDataWithAlert,
|
||||
mockTimelineDetails,
|
||||
mockTimelineResult,
|
||||
} from '../../../common/mock/';
|
||||
import { CreateTimeline, UpdateTimelineLoading } from './types';
|
||||
import { Ecs } from '../../../../common/ecs';
|
||||
|
@ -415,13 +416,60 @@ describe('alert actions', () => {
|
|||
});
|
||||
|
||||
test('it uses current time timestamp if ecsData.timestamp is not provided', () => {
|
||||
const { timestamp, ...ecsDataMock } = {
|
||||
...mockEcsDataWithAlert,
|
||||
};
|
||||
const { timestamp, ...ecsDataMock } = mockEcsDataWithAlert;
|
||||
const result = determineToAndFrom({ ecs: ecsDataMock });
|
||||
|
||||
expect(result.from).toEqual('2020-03-01T17:54:46.349Z');
|
||||
expect(result.to).toEqual('2020-03-01T17:59:46.349Z');
|
||||
});
|
||||
|
||||
test('it uses original_time and threshold_result.from for threshold alerts', async () => {
|
||||
const ecsDataMock = getThresholdDetectionAlertAADMock();
|
||||
|
||||
const expectedFrom = '2021-01-10T21:11:45.839Z';
|
||||
const expectedTo = '2021-01-10T21:12:45.839Z';
|
||||
|
||||
await sendAlertToTimelineAction({
|
||||
createTimeline,
|
||||
ecsData: ecsDataMock,
|
||||
updateTimelineIsLoading,
|
||||
searchStrategyClient,
|
||||
});
|
||||
expect(createTimeline).toHaveBeenCalledTimes(1);
|
||||
expect(createTimeline).toHaveBeenCalledWith({
|
||||
...defaultTimelineProps,
|
||||
timeline: {
|
||||
...defaultTimelineProps.timeline,
|
||||
dataProviders: [
|
||||
{
|
||||
and: [],
|
||||
enabled: true,
|
||||
excluded: false,
|
||||
id: 'send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-destination-ip-1',
|
||||
kqlQuery: '',
|
||||
name: 'destination.ip',
|
||||
queryMatch: { field: 'destination.ip', operator: ':', value: 1 },
|
||||
},
|
||||
],
|
||||
dateRange: {
|
||||
start: expectedFrom,
|
||||
end: expectedTo,
|
||||
},
|
||||
description: '_id: 1',
|
||||
kqlQuery: {
|
||||
filterQuery: {
|
||||
kuery: {
|
||||
expression: ['user.id:1'],
|
||||
kind: ['kuery'],
|
||||
},
|
||||
serializedQuery: ['user.id:1'],
|
||||
},
|
||||
},
|
||||
resolveTimelineConfig: undefined,
|
||||
},
|
||||
from: expectedFrom,
|
||||
to: expectedTo,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
ALERT_RULE_FROM,
|
||||
ALERT_RULE_TYPE,
|
||||
ALERT_RULE_NOTE,
|
||||
ALERT_RULE_PARAMETERS,
|
||||
} from '@kbn/rule-data-utils/technical_field_names';
|
||||
|
||||
import {
|
||||
|
@ -27,7 +28,6 @@ import {
|
|||
ALERT_THRESHOLD_RESULT,
|
||||
} from '../../../../common/field_maps/field_names';
|
||||
import {
|
||||
KueryFilterQueryKind,
|
||||
TimelineId,
|
||||
TimelineResult,
|
||||
TimelineStatus,
|
||||
|
@ -157,63 +157,64 @@ const getFiltersFromRule = (filters: string[]): Filter[] =>
|
|||
}
|
||||
}, [] as Filter[]);
|
||||
|
||||
const calculateFromTimeFallback = (thresholdData: Ecs, originalTime: moment.Moment) => {
|
||||
// relative time that the rule's time range starts at (e.g. now-1h)
|
||||
|
||||
const ruleFromValue = getField(thresholdData, ALERT_RULE_FROM);
|
||||
const normalizedRuleFromValue = Array.isArray(ruleFromValue) ? ruleFromValue[0] : ruleFromValue;
|
||||
const ruleFrom = dateMath.parse(normalizedRuleFromValue);
|
||||
|
||||
// get the absolute (moment.duration) interval by subtracting `ruleFrom` from `now`
|
||||
const now = moment();
|
||||
const ruleInterval = moment.duration(now.diff(ruleFrom));
|
||||
|
||||
// subtract the rule interval from the time the alert was generated... this will
|
||||
// overshoot and potentially contain false positives in the timeline results
|
||||
return originalTime.clone().subtract(ruleInterval);
|
||||
};
|
||||
|
||||
export const getThresholdAggregationData = (ecsData: Ecs | Ecs[]): ThresholdAggregationData => {
|
||||
const thresholdEcsData: Ecs[] = Array.isArray(ecsData) ? ecsData : [ecsData];
|
||||
return thresholdEcsData.reduce<ThresholdAggregationData>(
|
||||
(outerAcc, thresholdData) => {
|
||||
const threshold = thresholdData.signal?.rule?.threshold as string[];
|
||||
const threshold =
|
||||
getField(thresholdData, ALERT_RULE_PARAMETERS).threshold ??
|
||||
thresholdData.signal?.rule?.threshold;
|
||||
|
||||
let aggField: string[] = [];
|
||||
let thresholdResult: {
|
||||
terms?: Array<{
|
||||
field?: string;
|
||||
const thresholdResult: {
|
||||
terms: Array<{
|
||||
field: string;
|
||||
value: string;
|
||||
}>;
|
||||
count: number;
|
||||
from: string;
|
||||
};
|
||||
} = getField(thresholdData, ALERT_THRESHOLD_RESULT);
|
||||
|
||||
try {
|
||||
thresholdResult = JSON.parse(
|
||||
(getField(thresholdData, ALERT_THRESHOLD_RESULT) as string[])[0]
|
||||
);
|
||||
aggField = JSON.parse(threshold[0]).field;
|
||||
} catch (err) {
|
||||
// Legacy support
|
||||
thresholdResult = {
|
||||
terms: [
|
||||
{
|
||||
field: (thresholdData.rule?.threshold as { field: string }).field,
|
||||
value: (thresholdData.signal?.threshold_result as { value: string }).value,
|
||||
},
|
||||
],
|
||||
count: (thresholdData.signal?.threshold_result as { count: number }).count,
|
||||
from: (thresholdData.signal?.threshold_result as { from: string }).from,
|
||||
};
|
||||
}
|
||||
// timestamp representing when the alert was generated
|
||||
const originalTimeValue = getField(thresholdData, ALERT_ORIGINAL_TIME);
|
||||
const normalizedOriginalTimeValue = Array.isArray(originalTimeValue)
|
||||
? originalTimeValue[0]
|
||||
: originalTimeValue;
|
||||
const originalTime = moment(normalizedOriginalTimeValue);
|
||||
|
||||
// Legacy support
|
||||
const ruleFromStr = getField(thresholdData, ALERT_RULE_FROM)[0];
|
||||
const ruleFrom = dateMath.parse(ruleFromStr) ?? moment(); // The fallback here will essentially ensure 0 results
|
||||
const originalTimeStr = getField(thresholdData, ALERT_ORIGINAL_TIME)[0];
|
||||
const originalTime = originalTimeStr != null ? moment(originalTimeStr) : ruleFrom;
|
||||
const ruleInterval = moment.duration(moment().diff(ruleFrom));
|
||||
const fromOriginalTime = originalTime.clone().subtract(ruleInterval); // This is the default... can overshoot
|
||||
// End legacy support
|
||||
/*
|
||||
* Compute the fallback interval when `threshold_result.from` is not available
|
||||
* (for pre-7.12 backcompat)
|
||||
*/
|
||||
const fromOriginalTime = calculateFromTimeFallback(thresholdData, originalTime);
|
||||
|
||||
const aggregationFields = Array.isArray(aggField) ? aggField : [aggField];
|
||||
const aggregationFields: string[] = Array.isArray(threshold.field)
|
||||
? threshold.field
|
||||
: [threshold.field];
|
||||
|
||||
return {
|
||||
// Use `threshold_result.from` if available (it will always be available for new signals). Otherwise, use a calculated
|
||||
// lower bound, which could result in the timeline showing a superset of the events that made up the threshold set.
|
||||
thresholdFrom: thresholdResult.from ?? fromOriginalTime.toISOString(),
|
||||
thresholdTo: originalTime.toISOString(),
|
||||
dataProviders: [
|
||||
...outerAcc.dataProviders,
|
||||
...aggregationFields.reduce<DataProvider[]>((acc, aggregationField, i) => {
|
||||
const aggregationValue = (thresholdResult.terms ?? []).filter(
|
||||
(term: { field?: string | undefined; value: string }) =>
|
||||
term.field === aggregationField
|
||||
const aggregationValue = thresholdResult.terms.filter(
|
||||
(term) => term.field === aggregationField
|
||||
)[0].value;
|
||||
const dataProviderValue = Array.isArray(aggregationValue)
|
||||
? aggregationValue[0]
|
||||
|
@ -265,7 +266,10 @@ export const isEqlRuleWithGroupId = (ecsData: Ecs) => {
|
|||
|
||||
export const isThresholdRule = (ecsData: Ecs) => {
|
||||
const ruleType = getField(ecsData, ALERT_RULE_TYPE);
|
||||
return Array.isArray(ruleType) && ruleType.length && ruleType[0] === 'threshold';
|
||||
return (
|
||||
ruleType === 'threshold' ||
|
||||
(Array.isArray(ruleType) && ruleType.length && ruleType[0] === 'threshold')
|
||||
);
|
||||
};
|
||||
|
||||
export const buildAlertsKqlFilter = (
|
||||
|
@ -476,16 +480,22 @@ export const sendAlertToTimelineAction = async ({
|
|||
if (isThresholdRule(ecsData)) {
|
||||
const { thresholdFrom, thresholdTo, dataProviders } = getThresholdAggregationData(ecsData);
|
||||
|
||||
const params = getField(ecsData, ALERT_RULE_PARAMETERS);
|
||||
const filters = getFiltersFromRule(params.filters ?? ecsData.signal?.rule?.filters) ?? [];
|
||||
const language = params.language ?? ecsData.signal?.rule?.language ?? 'kuery';
|
||||
const query = params.query ?? ecsData.signal?.rule?.query ?? '';
|
||||
const indexNames = params.index ?? ecsData.signal?.rule?.index ?? [];
|
||||
|
||||
return createTimeline({
|
||||
from: thresholdFrom,
|
||||
notes: null,
|
||||
timeline: {
|
||||
...timelineDefaults,
|
||||
description: `_id: ${ecsData._id}`,
|
||||
filters: getFiltersFromRule(ecsData.signal?.rule?.filters as string[]),
|
||||
filters,
|
||||
dataProviders,
|
||||
id: TimelineId.active,
|
||||
indexNames: [],
|
||||
indexNames,
|
||||
dateRange: {
|
||||
start: thresholdFrom,
|
||||
end: thresholdTo,
|
||||
|
@ -494,14 +504,10 @@ export const sendAlertToTimelineAction = async ({
|
|||
kqlQuery: {
|
||||
filterQuery: {
|
||||
kuery: {
|
||||
kind: ecsData.signal?.rule?.language?.length
|
||||
? (ecsData.signal?.rule?.language[0] as KueryFilterQueryKind)
|
||||
: 'kuery',
|
||||
expression: ecsData.signal?.rule?.query?.length ? ecsData.signal?.rule?.query[0] : '',
|
||||
kind: language,
|
||||
expression: query,
|
||||
},
|
||||
serializedQuery: ecsData.signal?.rule?.query?.length
|
||||
? ecsData.signal?.rule?.query[0]
|
||||
: '',
|
||||
serializedQuery: query,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -20,6 +20,7 @@ const ecsRowData: Ecs = {
|
|||
alert: {
|
||||
workflow_status: ['open'],
|
||||
rule: {
|
||||
parameters: {},
|
||||
uuid: ['testId'],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -64,8 +64,7 @@ export const useInvestigateInTimeline = ({
|
|||
timeline: {
|
||||
...timeline,
|
||||
filterManager,
|
||||
// by setting as an empty array, it will default to all in the reducer because of the event type
|
||||
indexNames: [],
|
||||
indexNames: timeline.indexNames ?? [],
|
||||
show: true,
|
||||
},
|
||||
to: toTimeline,
|
||||
|
@ -78,7 +77,7 @@ export const useInvestigateInTimeline = ({
|
|||
const showInvestigateInTimelineAction = alertIds != null;
|
||||
const { isLoading: isFetchingAlertEcs, alertsEcsData } = useFetchEcsAlertsData({
|
||||
alertIds,
|
||||
skip: ecsRowData != null || alertIds == null,
|
||||
skip: alertIds == null,
|
||||
});
|
||||
|
||||
const investigateInTimelineAlertClick = useCallback(async () => {
|
||||
|
@ -92,9 +91,7 @@ export const useInvestigateInTimeline = ({
|
|||
searchStrategyClient,
|
||||
updateTimelineIsLoading,
|
||||
});
|
||||
}
|
||||
|
||||
if (ecsRowData != null) {
|
||||
} else if (ecsRowData != null) {
|
||||
await sendAlertToTimelineAction({
|
||||
createTimeline,
|
||||
ecsData: ecsRowData,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALERT_RULE_UUID } from '@kbn/rule-data-utils/technical_field_names';
|
||||
import { ALERT_RULE_UUID, ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils/technical_field_names';
|
||||
import { get, isEmpty } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { matchPath, RouteProps, Redirect } from 'react-router-dom';
|
||||
|
@ -213,10 +213,16 @@ RedirectRoute.displayName = 'RedirectRoute';
|
|||
|
||||
const siemSignalsFieldMappings: Record<string, string> = {
|
||||
[ALERT_RULE_UUID]: 'signal.rule.id',
|
||||
[`${ALERT_RULE_PARAMETERS}.filters`]: 'signal.rule.filters',
|
||||
[`${ALERT_RULE_PARAMETERS}.language`]: 'signal.rule.language',
|
||||
[`${ALERT_RULE_PARAMETERS}.query`]: 'signal.rule.query',
|
||||
};
|
||||
|
||||
const alertFieldMappings: Record<string, string> = {
|
||||
'signal.rule.id': ALERT_RULE_UUID,
|
||||
'signal.rule.filters': `${ALERT_RULE_PARAMETERS}.filters`,
|
||||
'signal.rule.language': `${ALERT_RULE_PARAMETERS}.language`,
|
||||
'signal.rule.query': `${ALERT_RULE_PARAMETERS}.query`,
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -232,5 +238,19 @@ export const getField = (ecsData: Ecs, field: string) => {
|
|||
'kibana.alert',
|
||||
'signal'
|
||||
);
|
||||
return get(aadField, ecsData) ?? get(siemSignalsField, ecsData);
|
||||
const parts = aadField.split('.');
|
||||
if (parts.includes('parameters') && parts[parts.length - 1] !== 'parameters') {
|
||||
const paramsField = parts.slice(0, parts.length - 1).join('.');
|
||||
const params = get(paramsField, ecsData);
|
||||
const value = get(parts[parts.length - 1], params);
|
||||
if (isEmpty(value)) {
|
||||
return [];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
const value = get(aadField, ecsData) ?? get(siemSignalsField, ecsData);
|
||||
if (isEmpty(value)) {
|
||||
return [];
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
|
|
@ -126,7 +126,7 @@ describe('Actions', () => {
|
|||
test('it enables for eventType=signal', () => {
|
||||
const ecsData = {
|
||||
...mockTimelineData[0].ecs,
|
||||
kibana: { alert: { rule: { uuid: ['123'] } } },
|
||||
kibana: { alert: { rule: { uuid: ['123'], parameters: {} } } },
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
|
|
@ -68,6 +68,7 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
|||
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
|
||||
const emptyNotes: string[] = [];
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const alertIds = useMemo(() => [ecsData._id], [ecsData]);
|
||||
|
||||
const onPinEvent: OnPinEvent = useCallback(
|
||||
(evtId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId: evtId })),
|
||||
|
@ -166,6 +167,7 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
|||
<InvestigateInTimelineAction
|
||||
ariaLabel={i18n.SEND_ALERT_TO_TIMELINE_FOR_ROW({ ariaRowindex, columnValues })}
|
||||
key="investigate-in-timeline"
|
||||
alertIds={alertIds}
|
||||
ecsRowData={ecsData}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -278,7 +278,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
|
|||
errors: result.errors.concat(runResult.errors),
|
||||
lastLookbackDate: runResult.lastLookBackDate,
|
||||
searchAfterTimes: result.searchAfterTimes.concat(runResult.searchAfterTimes),
|
||||
state: runState,
|
||||
state: runResult.state,
|
||||
success: result.success && runResult.success,
|
||||
warning: warningMessages.length > 0,
|
||||
warningMessages,
|
||||
|
|
|
@ -43,7 +43,6 @@ export const getThresholdSignalHistory = async ({
|
|||
signalHistory: ThresholdSignalHistory;
|
||||
searchErrors: string[];
|
||||
}> => {
|
||||
// TODO: use ruleDataClient.getReader()
|
||||
const { searchResult, searchErrors } = await findPreviousThresholdSignals({
|
||||
indexPattern,
|
||||
from,
|
||||
|
|
|
@ -27,7 +27,7 @@ import { TimelineType } from '../../../../../../common/types/timeline';
|
|||
|
||||
export const cleanDraftTimelinesRoute = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
config: ConfigType,
|
||||
_: ConfigType,
|
||||
security: SetupPlugins['security']
|
||||
) => {
|
||||
router.post(
|
||||
|
|
|
@ -29,7 +29,7 @@ export * from './helpers';
|
|||
|
||||
export const createTimelinesRoute = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
config: ConfigType,
|
||||
_: ConfigType,
|
||||
security: SetupPlugins['security']
|
||||
) => {
|
||||
router.post(
|
||||
|
|
|
@ -23,7 +23,7 @@ import { CompareTimelinesStatus } from '../../../utils/compare_timelines_status'
|
|||
|
||||
export const patchTimelinesRoute = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
config: ConfigType,
|
||||
_: ConfigType,
|
||||
security: SetupPlugins['security']
|
||||
) => {
|
||||
router.patch(
|
||||
|
|
|
@ -32,7 +32,7 @@ import { ThreatEcs } from './threat';
|
|||
import { Ransomware } from './ransomware';
|
||||
|
||||
export type SignalEcsAAD = Exclude<SignalEcs, 'rule' | 'status'> & {
|
||||
rule?: Exclude<RuleEcs, 'id'> & { uuid: string[] };
|
||||
rule?: Exclude<RuleEcs, 'id'> & { parameters: Record<string, unknown>; uuid: string[] };
|
||||
building_block_type?: string[];
|
||||
workflow_status?: string[];
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue