Skip to content
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][RAC][Cases] Fix RAC "add to case" functionality from alerts table #116768

Merged
merged 9 commits into from
Nov 3, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('Alerts timeline', () => {
loadDetectionsPage(ROLES.platform_engineer);
});

it.skip('should allow a user with crud privileges to attach alerts to cases', () => {
it('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 @@ -50,6 +50,7 @@ import {
QueryOperator,
} from '../../../timelines/components/timeline/data_providers/data_provider';
import { getTimelineTemplate } from '../../../timelines/containers/api';
import { getField } from '../../../helpers';

export const getUpdateAlertsQuery = (eventIds: Readonly<string[]>) => {
return {
Expand All @@ -68,10 +69,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 +143,9 @@ export const determineToAndFrom = ({ ecs }: { ecs: Ecs[] | Ecs }) => {
};
}
const ecsData = ecs as Ecs;
const ruleFrom = getField(ecsData, 'signal.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 +169,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 Down Expand Up @@ -192,11 +201,11 @@ export const getThresholdAggregationData = (
};
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const originalTime = moment(thresholdData.signal?.original_time![0]);
const originalTimeStr = getField(thresholdData, 'signal.original_time')[0];
madirey marked this conversation as resolved.
Show resolved Hide resolved
const originalTime = moment(originalTimeStr);
const now = moment();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const ruleFrom = dateMath.parse(thresholdData.signal?.rule?.from![0]!);
const ruleFromStr = getField(thresholdData, 'signal.rule.from')[0];
const ruleFrom = dateMath.parse(ruleFromStr);
const ruleInterval = moment.duration(now.diff(ruleFrom));
const fromOriginalTime = originalTime.clone().subtract(ruleInterval); // This is the default... can overshoot
const aggregationFields = Array.isArray(aggField) ? aggField : [aggField];
Expand Down Expand Up @@ -255,16 +264,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, 'signal.rule.type');
const groupId = getField(ecsData, 'signal.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, 'signal.rule.type');
return 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 +295,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 +395,10 @@ 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 timelineId =
ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : '';
const ruleNote = getField(ecsData, 'signal.rule.note')[0];
madirey marked this conversation as resolved.
Show resolved Hide resolved
const noteContent = ruleNote ?? '';
const ruleTimelineId = getField(ecsData, 'signal.rule.timeline_id');
madirey marked this conversation as resolved.
Show resolved Hide resolved
const timelineId = ruleTimelineId ?? '';
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
14 changes: 13 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,13 @@ export const RedirectRoute = React.memo<{ capabilities: Capabilities }>(({ capab
return <Redirect to={OVERVIEW_PATH} />;
});
RedirectRoute.displayName = 'RedirectRoute';

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

export const getField = (ecsData: Ecs, field: string) => {
return (
get(field, ecsData) ?? get(racFieldMappings[field].replace('signal', 'kibana.alert'), ecsData)
);
};
2 changes: 1 addition & 1 deletion x-pack/plugins/timelines/public/hooks/use_add_to_case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export const useAddToCase = ({
}
}, [event]);
const isSecurityAlert = useMemo(() => {
return !isEmpty(event?.ecs.signal?.rule?.id);
return !isEmpty(event?.ecs.signal?.rule?.id ?? event?.ecs.kibana?.alert?.rule?.uuid);
}, [event]);
const isEventSupported = isSecurityAlert || isAlert;
const userCanCrud = casePermissions?.crud ?? false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
};

return {
testFiles: [require.resolve(`../${name}/tests/`)],
testFiles: [require.resolve(`../${name}/tests/attach_to_case`)],
servers,
services,
junit: {
Expand Down