[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:
Madison Caldwell 2022-01-11 16:35:04 -05:00 committed by GitHub
parent ecfd50b11b
commit 9e80a2f6c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 435 additions and 74 deletions

View file

@ -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[];
};

View file

@ -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[];
};

View file

@ -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,
},

View file

@ -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';

View file

@ -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,
},
];

View 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;
}

View file

@ -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,
});
});
});
});

View file

@ -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,
},
},
},

View file

@ -20,6 +20,7 @@ const ecsRowData: Ecs = {
alert: {
workflow_status: ['open'],
rule: {
parameters: {},
uuid: ['testId'],
},
},

View file

@ -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,

View file

@ -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;
};

View file

@ -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>

View file

@ -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}
/>
)}

View file

@ -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,

View file

@ -43,7 +43,6 @@ export const getThresholdSignalHistory = async ({
signalHistory: ThresholdSignalHistory;
searchErrors: string[];
}> => {
// TODO: use ruleDataClient.getReader()
const { searchResult, searchErrors } = await findPreviousThresholdSignals({
indexPattern,
from,

View file

@ -27,7 +27,7 @@ import { TimelineType } from '../../../../../../common/types/timeline';
export const cleanDraftTimelinesRoute = (
router: SecuritySolutionPluginRouter,
config: ConfigType,
_: ConfigType,
security: SetupPlugins['security']
) => {
router.post(

View file

@ -29,7 +29,7 @@ export * from './helpers';
export const createTimelinesRoute = (
router: SecuritySolutionPluginRouter,
config: ConfigType,
_: ConfigType,
security: SetupPlugins['security']
) => {
router.post(

View file

@ -23,7 +23,7 @@ import { CompareTimelinesStatus } from '../../../utils/compare_timelines_status'
export const patchTimelinesRoute = (
router: SecuritySolutionPluginRouter,
config: ConfigType,
_: ConfigType,
security: SetupPlugins['security']
) => {
router.patch(

View file

@ -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[];
};