Skip to content

Commit

Permalink
[Security Solution][RAC][Cases] Fix RAC "add to case" functionality f…
Browse files Browse the repository at this point in the history
…rom alerts table (elastic#116768)

* Fix add to case functionality

* Use appropriate owner when attaching an alert to a case

* Use field name constants

* Gotta reskip the test

* Better error handling

* Fix type errors

* Fix tests
  • Loading branch information
madirey authored Nov 3, 2021
1 parent d38f488 commit bd97d1f
Show file tree
Hide file tree
Showing 25 changed files with 140 additions and 65 deletions.
9 changes: 9 additions & 0 deletions x-pack/plugins/cases/common/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,19 @@ export interface SignalEcs {
threshold_result?: unknown;
}

export type SignalEcsAAD = Exclude<SignalEcs, 'rule' | 'status'> & {
rule?: Exclude<RuleEcs, 'id'> & { uuid: string[] };
building_block_type?: string[];
workflow_status?: string[];
};

export interface Ecs {
_id: string;
_index?: string;
signal?: SignalEcs;
kibana?: {
alert: SignalEcsAAD;
};
}

export type CaseActionConnector = ActionConnector;
14 changes: 11 additions & 3 deletions x-pack/plugins/cases/public/components/user_action_tree/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import {
EuiCommentList,
EuiCommentProps,
} from '@elastic/eui';
import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';

import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { get, isEmpty } from 'lodash';
import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import styled from 'styled-components';
Expand Down Expand Up @@ -421,9 +423,15 @@ export const UserActionTree = React.memo(
}

const ruleId =
comment?.rule?.id ?? manualAlertsData[alertId]?.signal?.rule?.id?.[0] ?? null;
comment?.rule?.id ??
manualAlertsData[alertId]?.signal?.rule?.id?.[0] ??
get(manualAlertsData[alertId], ALERT_RULE_UUID)[0] ??
null;
const ruleName =
comment?.rule?.name ?? manualAlertsData[alertId]?.signal?.rule?.name?.[0] ?? null;
comment?.rule?.name ??
manualAlertsData[alertId]?.signal?.rule?.name?.[0] ??
get(manualAlertsData[alertId], ALERT_RULE_NAME)[0] ??
null;

return [
...comments,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { FieldMap } from '../../../../../../rule_registry/common/field_map';
import { FieldMap } from '../../../rule_registry/common/field_map';

export const alertsFieldMap: FieldMap = {
'kibana.alert.ancestors': {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const ALERT_DEPTH = `${ALERT_NAMESPACE}.depth` as const;
export const ALERT_GROUP_ID = `${ALERT_NAMESPACE}.group.id` as const;
export const ALERT_GROUP_INDEX = `${ALERT_NAMESPACE}.group.index` as const;
export const ALERT_ORIGINAL_TIME = `${ALERT_NAMESPACE}.original_time` as const;
export const ALERT_THRESHOLD_RESULT = `${ALERT_NAMESPACE}.threshold_result` as const;

export const ALERT_ORIGINAL_EVENT = `${ALERT_NAMESPACE}.original_event` as const;
export const ALERT_ORIGINAL_EVENT_ACTION = `${ALERT_ORIGINAL_EVENT}.action` as const;
Expand All @@ -24,3 +25,4 @@ export const ALERT_ORIGINAL_EVENT_TYPE = `${ALERT_ORIGINAL_EVENT}.type` as const

export const ALERT_RULE_THRESHOLD = `${ALERT_RULE_NAMESPACE}.threshold` as const;
export const ALERT_RULE_THRESHOLD_FIELD = `${ALERT_RULE_THRESHOLD}.field` as const;
export const ALERT_RULE_TIMELINE_ID = `${ALERT_RULE_NAMESPACE}.timeline_id` as const;
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ describe('Alerts timeline', () => {
loadDetectionsPage(ROLES.platform_engineer);
});

// Skipping due to alerts not refreshing for platform_engineer despite being returned from API?
it.skip('should allow a user with crud privileges to attach alerts to cases', () => {
cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true });
cy.get(ATTACH_ALERT_TO_CASE_BUTTON).first().should('not.be.disabled');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@

/* eslint-disable complexity */

import dateMath from '@elastic/datemath';
import { getOr, isEmpty } from 'lodash/fp';
import { get, getOr, isEmpty } from 'lodash/fp';
import moment from 'moment';
import { i18n } from '@kbn/i18n';

import dateMath from '@elastic/datemath';

import { FilterStateStore, Filter } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { ALERT_RULE_FROM, ALERT_RULE_TYPE, ALERT_RULE_NOTE } from '@kbn/rule-data-utils';

import {
ALERT_ORIGINAL_TIME,
ALERT_GROUP_ID,
ALERT_RULE_TIMELINE_ID,
ALERT_THRESHOLD_RESULT,
} from '../../../../common/field_maps/field_names';
import {
KueryFilterQueryKind,
TimelineId,
Expand Down Expand Up @@ -40,6 +49,7 @@ import {
formatTimelineResultToModel,
} from '../../../timelines/components/open_timeline/helpers';
import { convertKueryToElasticSearchQuery } from '../../../common/lib/keury';
import { getField } from '../../../helpers';
import {
replaceTemplateFieldFromQuery,
replaceTemplateFieldFromMatchFilters,
Expand Down Expand Up @@ -68,10 +78,18 @@ export const getUpdateAlertsQuery = (eventIds: Readonly<string[]>) => {
export const getFilterAndRuleBounds = (
data: TimelineNonEcsData[][]
): [string[], number, number] => {
const stringFilter = data?.[0].filter((d) => d.field === 'signal.rule.filters')?.[0]?.value ?? [];
const stringFilter =
data?.[0].filter(
(d) => d.field === 'signal.rule.filters' || d.field === 'kibana.alert.rule.filters'
)?.[0]?.value ?? [];

const eventTimes = data
.flatMap((alert) => alert.filter((d) => d.field === 'signal.original_time')?.[0]?.value ?? [])
.flatMap(
(alert) =>
alert.filter(
(d) => d.field === 'signal.original_time' || d.field === 'kibana.alert.original_time'
)?.[0]?.value ?? []
)
.map((d) => moment(d));

return [stringFilter, moment.min(eventTimes).valueOf(), moment.max(eventTimes).valueOf()];
Expand Down Expand Up @@ -134,10 +152,9 @@ export const determineToAndFrom = ({ ecs }: { ecs: Ecs[] | Ecs }) => {
};
}
const ecsData = ecs as Ecs;
const ruleFrom = getField(ecsData, ALERT_RULE_FROM);
const elapsedTimeRule = moment.duration(
moment().diff(
dateMath.parse(ecsData?.signal?.rule?.from != null ? ecsData.signal?.rule?.from[0] : 'now-0s')
)
moment().diff(dateMath.parse(ruleFrom != null ? ruleFrom[0] : 'now-0s'))
);
const from = moment(ecsData?.timestamp ?? new Date())
.subtract(elapsedTimeRule)
Expand All @@ -161,6 +178,7 @@ export const getThresholdAggregationData = (
ecsData: Ecs | Ecs[],
nonEcsData: TimelineNonEcsData[]
): ThresholdAggregationData => {
// TODO: AAD fields
const thresholdEcsData: Ecs[] = Array.isArray(ecsData) ? ecsData : [ecsData];
return thresholdEcsData.reduce<ThresholdAggregationData>(
(outerAcc, thresholdData) => {
Expand All @@ -177,9 +195,14 @@ export const getThresholdAggregationData = (
};

try {
thresholdResult = JSON.parse((thresholdData.signal?.threshold_result as string[])[0]);
try {
thresholdResult = JSON.parse((thresholdData.signal?.threshold_result as string[])[0]);
} catch (err) {
thresholdResult = JSON.parse((get(ALERT_THRESHOLD_RESULT, thresholdData) as string[])[0]);
}
aggField = JSON.parse(threshold[0]).field;
} catch (err) {
// Legacy support
thresholdResult = {
terms: [
{
Expand All @@ -192,13 +215,15 @@ export const getThresholdAggregationData = (
};
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const originalTime = moment(thresholdData.signal?.original_time![0]);
const now = moment();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const ruleFrom = dateMath.parse(thresholdData.signal?.rule?.from![0]!);
const ruleInterval = moment.duration(now.diff(ruleFrom));
// 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

const aggregationFields = Array.isArray(aggField) ? aggField : [aggField];

return {
Expand Down Expand Up @@ -255,16 +280,19 @@ export const getThresholdAggregationData = (
);
};

export const isEqlRuleWithGroupId = (ecsData: Ecs) =>
ecsData.signal?.rule?.type?.length &&
ecsData.signal?.rule?.type[0] === 'eql' &&
ecsData.signal?.group?.id?.length;
export const isEqlRuleWithGroupId = (ecsData: Ecs) => {
const ruleType = getField(ecsData, ALERT_RULE_TYPE);
const groupId = getField(ecsData, ALERT_GROUP_ID);
return ruleType?.length && ruleType[0] === 'eql' && groupId?.length;
};

export const isThresholdRule = (ecsData: Ecs) =>
ecsData.signal?.rule?.type?.length && ecsData.signal?.rule?.type[0] === 'threshold';
export const isThresholdRule = (ecsData: Ecs) => {
const ruleType = getField(ecsData, ALERT_RULE_TYPE);
return Array.isArray(ruleType) && ruleType.length && ruleType[0] === 'threshold';
};

export const buildAlertsKqlFilter = (
key: '_id' | 'signal.group.id',
key: '_id' | 'signal.group.id' | 'kibana.alert.group.id',
alertIds: string[]
): Filter[] => {
return [
Expand All @@ -283,7 +311,7 @@ export const buildAlertsKqlFilter = (
negate: false,
disabled: false,
type: 'phrases',
key,
key: key.replace('signal.', 'kibana.alert.'),
value: alertIds.join(),
params: alertIds,
},
Expand Down Expand Up @@ -383,9 +411,11 @@ export const sendAlertToTimelineAction = async ({
*/
const ecsData: Ecs = Array.isArray(ecs) && ecs.length > 0 ? ecs[0] : (ecs as Ecs);
const alertIds = Array.isArray(ecs) ? ecs.map((d) => d._id) : [];
const noteContent = ecsData.signal?.rule?.note != null ? ecsData.signal?.rule?.note[0] : '';
const ruleNote = getField(ecsData, ALERT_RULE_NOTE);
const noteContent = Array.isArray(ruleNote) && ruleNote.length > 0 ? ruleNote[0] : '';
const ruleTimelineId = getField(ecsData, ALERT_RULE_TIMELINE_ID);
const timelineId =
ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : '';
Array.isArray(ruleTimelineId) && ruleTimelineId.length > 0 ? ruleTimelineId[0] : '';
const { to, from } = determineToAndFrom({ ecs });

// For now we do not want to populate the template timeline if we have alertIds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { useMemo } from 'react';
import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana';
import { TimelineId, TimelineNonEcsData } from '../../../../../common';
import { APP_UI_ID } from '../../../../../common/constants';
import { APP_ID } from '../../../../../common/constants';
import { useInsertTimeline } from '../../../../cases/components/use_insert_timeline';
import { Ecs } from '../../../../../common/ecs';

Expand Down Expand Up @@ -39,7 +39,7 @@ export const useAddToCaseActions = ({
event: { data: nonEcsData ?? [], ecs: ecsData, _id: ecsData?._id },
useInsertTimeline: insertTimelineHook,
casePermissions,
appId: APP_UI_ID,
appId: APP_ID,
onClose: afterCaseSelection,
}
: null,
Expand Down
28 changes: 27 additions & 1 deletion x-pack/plugins/security_solution/public/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* 2.0.
*/

import { isEmpty } from 'lodash/fp';
import { ALERT_RULE_UUID } from '@kbn/rule-data-utils';
import { get, isEmpty } from 'lodash/fp';
import React from 'react';
import { matchPath, RouteProps, Redirect } from 'react-router-dom';

Expand All @@ -22,6 +23,7 @@ import {
OVERVIEW_PATH,
CASES_PATH,
} from '../common/constants';
import { Ecs } from '../common/ecs';
import {
FactoryQueryTypes,
StrategyResponseType,
Expand Down Expand Up @@ -208,3 +210,27 @@ export const RedirectRoute = React.memo<{ capabilities: Capabilities }>(({ capab
return <Redirect to={OVERVIEW_PATH} />;
});
RedirectRoute.displayName = 'RedirectRoute';

const siemSignalsFieldMappings: Record<string, string> = {
[ALERT_RULE_UUID]: 'signal.rule.id',
};

const alertFieldMappings: Record<string, string> = {
'signal.rule.id': ALERT_RULE_UUID,
};

/*
* Deprecation notice: This functionality should be removed when support for signal.* is no longer
* supported.
*
* Selectively returns the AAD field value (kibana.alert.*) or the legacy field value
* (signal.*), whichever is present. For backwards compatibility.
*/
export const getField = (ecsData: Ecs, field: string) => {
const aadField = (alertFieldMappings[field] ?? field).replace('signal', 'kibana.alert');
const siemSignalsField = (siemSignalsFieldMappings[field] ?? field).replace(
'kibana.alert',
'signal'
);
return get(aadField, ecsData) ?? get(siemSignalsField, ecsData);
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ import { TypeOfFieldMap } from '../../../../../../rule_registry/common/field_map
import { SERVER_APP_ID } from '../../../../../common/constants';
import { ANCHOR_DATE } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock';
import { RulesFieldMap } from '../field_maps';
import { RulesFieldMap } from '../../../../../common/field_maps';
import {
ALERT_ANCESTORS,
ALERT_ORIGINAL_TIME,
ALERT_ORIGINAL_EVENT,
} from '../field_maps/field_names';
} from '../../../../../common/field_maps/field_names';
import { WrappedRACAlert } from '../types';

export const mockThresholdResults = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ import {
ANCHOR_DATE,
} from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
import { getListArrayMock } from '../../../../../../common/detection_engine/schemas/types/lists.mock';
import { SERVER_APP_ID } from '../../../../../../common/constants';
import { EVENT_DATASET } from '../../../../../../common/cti/constants';
import {
ALERT_ANCESTORS,
ALERT_ORIGINAL_TIME,
ALERT_DEPTH,
ALERT_ORIGINAL_EVENT,
ALERT_ORIGINAL_TIME,
} from '../../field_maps/field_names';
import { SERVER_APP_ID } from '../../../../../../common/constants';
import { EVENT_DATASET } from '../../../../../../common/cti/constants';
} from '../../../../../../common/field_maps/field_names';

type SignalDoc = SignalSourceHit & {
_source: Required<SignalSourceHit>['_source'] & { [TIMESTAMP]: string };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ import {
isWrappedSignalHit,
} from '../../../signals/utils';
import { RACAlert } from '../../types';
import { SERVER_APP_ID } from '../../../../../../common/constants';
import { SearchTypes } from '../../../../telemetry/types';
import {
ALERT_ANCESTORS,
ALERT_DEPTH,
ALERT_ORIGINAL_EVENT,
ALERT_ORIGINAL_TIME,
} from '../../field_maps/field_names';
import { SERVER_APP_ID } from '../../../../../../common/constants';
import { SearchTypes } from '../../../../telemetry/types';
ALERT_ORIGINAL_EVENT,
} from '../../../../../../common/field_maps/field_names';

export const generateAlertId = (alert: RACAlert) => {
return createHash('sha256')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import { ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils';

import { sampleDocNoSortId, sampleRuleGuid } from '../../../signals/__mocks__/es_results';
import { buildAlertGroupFromSequence } from './build_alert_group_from_sequence';
import { SERVER_APP_ID } from '../../../../../../common/constants';
import { getCompleteRuleMock, getQueryRuleParams } from '../../../schemas/rule_schemas.mock';
import { QueryRuleParams } from '../../../schemas/rule_schemas';
import {
ALERT_ANCESTORS,
ALERT_BUILDING_BLOCK_TYPE,
ALERT_DEPTH,
ALERT_BUILDING_BLOCK_TYPE,
ALERT_GROUP_ID,
} from '../../field_maps/field_names';
import { SERVER_APP_ID } from '../../../../../../common/constants';
import { getCompleteRuleMock, getQueryRuleParams } from '../../../schemas/rule_schemas.mock';
import { QueryRuleParams } from '../../../schemas/rule_schemas';
} from '../../../../../../common/field_maps/field_names';

const SPACE_ID = 'space';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ import { EqlSequence } from '../../../../../../common/detection_engine/types';
import { generateBuildingBlockIds } from './generate_building_block_ids';
import { objectArrayIntersection } from '../../../signals/build_bulk_body';
import { BuildReasonMessage } from '../../../signals/reason_formatters';
import { CompleteRule, RuleParams } from '../../../schemas/rule_schemas';
import {
ALERT_BUILDING_BLOCK_TYPE,
ALERT_GROUP_ID,
ALERT_GROUP_INDEX,
} from '../../field_maps/field_names';
import { CompleteRule, RuleParams } from '../../../schemas/rule_schemas';
} from '../../../../../../common/field_maps/field_names';

/**
* Takes N raw documents from ES that form a sequence and builds them into N+1 signals ready to be indexed -
Expand Down
Loading

0 comments on commit bd97d1f

Please sign in to comment.