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 @@ -24,3 +24,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,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 @@ -7,12 +7,20 @@

/* eslint-disable complexity */

import dateMath from '@elastic/datemath';
import { 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,
} from '../../../../common/field_maps/field_names';
import {
KueryFilterQueryKind,
TimelineId,
Expand Down Expand Up @@ -50,6 +58,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 +77,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 +151,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 +177,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 +209,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, ALERT_ORIGINAL_TIME)[0];
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, ALERT_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 +272,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 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 +303,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 +403,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, ALERT_RULE_NOTE)[0];
const noteContent = ruleNote ?? '';
const ruleTimelineId = getField(ecsData, ALERT_RULE_TIMELINE_ID);
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
20 changes: 19 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,19 @@ 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,
};

/*
* @deprecated 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 = (racFieldMappings[field] ?? field).replace('signal', 'kibana.alert');
return get(aadField, ecsData) ?? get(field, 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
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

import { ALERT_RULE_UUID } from '@kbn/rule-data-utils';
import { createHash } from 'crypto';
import { ALERT_ANCESTORS } from '../../../../../../common/field_maps/field_names';
import { Ancestor } from '../../../signals/types';
import { ALERT_ANCESTORS } from '../../field_maps/field_names';
import { RACAlert } from '../../types';

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

import { ALERT_ORIGINAL_TIME } from '../../rule_types/field_maps/field_names';
import { ALERT_ORIGINAL_TIME } from '../../../../../common/field_maps/field_names';
import { sampleThresholdAlert } from '../../rule_types/__mocks__/threshold';
import { buildThresholdSignalHistory } from './build_signal_history';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

import { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
ALERT_ORIGINAL_TIME,
ALERT_RULE_THRESHOLD_FIELD,
} from '../../rule_types/field_maps/field_names';
ALERT_ORIGINAL_TIME,
} from '../../../../../common/field_maps/field_names';

import { SimpleHit, ThresholdSignalHistory } from '../types';
import { getThresholdTermsHash, isWrappedRACAlert, isWrappedSignalHit } from '../utils';
Expand Down
3 changes: 1 addition & 2 deletions x-pack/plugins/security_solution/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ import { licenseService } from './lib/license';
import { PolicyWatcher } from './endpoint/lib/policy/license_watch';
import { migrateArtifactsToFleet } from './endpoint/lib/artifacts/migrate_artifacts_to_fleet';
import aadFieldConversion from './lib/detection_engine/routes/index/signal_aad_mapping.json';
import { alertsFieldMap } from './lib/detection_engine/rule_types/field_maps/alerts';
import { rulesFieldMap } from './lib/detection_engine/rule_types/field_maps/rules';
import { registerEventLogProvider } from './lib/detection_engine/rule_execution_log/event_log_adapter/register_event_log_provider';
import { getKibanaPrivilegesFeaturePrivileges, getCasesKibanaFeature } from './features';
import { EndpointMetadataService } from './endpoint/services/metadata';
Expand All @@ -88,6 +86,7 @@ import type {
SecuritySolutionPluginStart,
PluginInitializerContext,
} from './plugin_contract';
import { alertsFieldMap, rulesFieldMap } from '../common/field_maps';

export { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract';

Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
ALERT_ANCESTORS,
ALERT_DEPTH,
ALERT_ORIGINAL_TIME,
} from '../../../../plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names';
} from '../../../../plugins/security_solution/common/field_maps/field_names';

// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ import {
import { getCreateThreatMatchRulesSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock';
import { getThreatMatchingSchemaPartialMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks';
import { ENRICHMENT_TYPES } from '../../../../plugins/security_solution/common/cti/constants';
import { Ancestor } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types';
import {
ALERT_ANCESTORS,
ALERT_DEPTH,
ALERT_ORIGINAL_EVENT_ACTION,
ALERT_ORIGINAL_EVENT_CATEGORY,
ALERT_ORIGINAL_EVENT_MODULE,
ALERT_ORIGINAL_TIME,
} from '../../../../plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names';
import { Ancestor } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types';
} from '../../../../plugins/security_solution/common/field_maps/field_names';

const format = (value: unknown): string => JSON.stringify(value, null, 2);

Expand Down
Loading