Skip to content

Commit

Permalink
Fix alert content when pushing to connector
Browse files Browse the repository at this point in the history
  • Loading branch information
cnasikas committed Dec 22, 2020
1 parent 5708d4c commit 885bbd3
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -90,20 +90,24 @@ interface Signal {
rule: {
id: string;
name: string;
to: string;
from: string;
};
}

interface SignalHit {
_id: string;
_index: string;
_source: {
'@timestamp': string;
signal: Signal;
};
}

export type Alert = {
_id: string;
_index: string;
'@timestamp': string;
} & Signal;

export const CaseComponent = React.memo<CaseProps>(
Expand Down Expand Up @@ -153,6 +157,7 @@ export const CaseComponent = React.memo<CaseProps>(
[_id]: {
_id,
_index,
'@timestamp': _source['@timestamp'],
..._source.signal,
},
}),
Expand Down Expand Up @@ -291,6 +296,7 @@ export const CaseComponent = React.memo<CaseProps>(
updateCase: handleUpdateCase,
userCanCrud,
isValidConnector,
alerts,
});

const onSubmitConnector = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { CaseServices } from '../../containers/use_get_case_user_actions';
import { LinkAnchor } from '../../../common/components/links';
import { SecurityPageName } from '../../../app/types';
import { ErrorMessage } from '../callout/types';
import { Alert } from '../case_view';

export interface UsePushToService {
caseId: string;
Expand All @@ -31,6 +32,7 @@ export interface UsePushToService {
updateCase: (newCase: Case) => void;
userCanCrud: boolean;
isValidConnector: boolean;
alerts: Record<string, Alert>;
}

export interface ReturnUsePushToService {
Expand All @@ -47,6 +49,7 @@ export const usePushToService = ({
updateCase,
userCanCrud,
isValidConnector,
alerts,
}: UsePushToService): ReturnUsePushToService => {
const history = useHistory();
const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.case);
Expand All @@ -61,9 +64,10 @@ export const usePushToService = ({
caseServices,
connector,
updateCase,
alerts,
});
}
}, [caseId, caseServices, connector, postPushToService, updateCase]);
}, [alerts, caseId, caseServices, connector, postPushToService, updateCase]);

const goToConfigureCases = useCallback(
(ev) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import { i18n } from '@kbn/i18n';

export * from '../translations';

export const ERROR_TITLE = i18n.translate('xpack.securitySolution.containers.case.errorTitle', {
defaultMessage: 'Error fetching data',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,20 @@ import { CaseServices } from './use_get_case_user_actions';
import { CaseConnector, ConnectorTypes, CommentType } from '../../../../case/common/api';

jest.mock('./api');
jest.mock('../../common/components/link_to', () => {
const originalModule = jest.requireActual('../../common/components/link_to');
return {
...originalModule,
getTimelineTabsUrl: jest.fn(),
useFormatUrl: jest.fn().mockReturnValue({ formatUrl: jest.fn(), search: 'urlSearch' }),
};
});

describe('usePostPushToService', () => {
const abortCtrl = new AbortController();
const updateCase = jest.fn();
const formatUrl = jest.fn();

const samplePush = {
caseId: pushedCase.id,
caseServices: {
Expand All @@ -45,7 +55,21 @@ describe('usePostPushToService', () => {
fields: { issueType: 'Task', priority: 'Low', parent: null },
} as CaseConnector,
updateCase,
alerts: {
'alert-id-1': {
_id: 'alert-id-1',
_index: 'alert-index-1',
'@timestamp': '2020-11-20T15:35:28.373Z',
rule: {
id: 'rule-id-1',
name: 'Awesome rule',
from: 'now-360s',
to: 'now',
},
},
},
};

const sampleServiceRequestData = {
savedObjectId: pushedCase.id,
createdAt: pushedCase.createdAt,
Expand Down Expand Up @@ -142,11 +166,13 @@ describe('usePostPushToService', () => {
expect(spyOnPushToService).toBeCalledWith(
samplePush.connector.id,
samplePush.connector.type,
formatServiceRequestData(
basicCase,
samplePush.connector,
sampleCaseServices as CaseServices
),
formatServiceRequestData({
myCase: basicCase,
connector: samplePush.connector,
caseServices: sampleCaseServices as CaseServices,
alerts: samplePush.alerts,
formatUrl,
}),
abortCtrl.signal
);
});
Expand All @@ -162,6 +188,7 @@ describe('usePostPushToService', () => {
type: ConnectorTypes.none,
fields: null,
},
alerts: samplePush.alerts,
updateCase,
};
const spyOnPushToService = jest.spyOn(api, 'pushToService');
Expand All @@ -176,7 +203,13 @@ describe('usePostPushToService', () => {
expect(spyOnPushToService).toBeCalledWith(
samplePush2.connector.id,
samplePush2.connector.type,
formatServiceRequestData(basicCase, samplePush2.connector, {}),
formatServiceRequestData({
myCase: basicCase,
connector: samplePush2.connector,
caseServices: {},
alerts: samplePush.alerts,
formatUrl,
}),
abortCtrl.signal
);
});
Expand Down Expand Up @@ -213,7 +246,13 @@ describe('usePostPushToService', () => {

it('formatServiceRequestData - current connector', () => {
const caseServices = sampleCaseServices;
const result = formatServiceRequestData(pushedCase, samplePush.connector, caseServices);
const result = formatServiceRequestData({
myCase: pushedCase,
connector: samplePush.connector,
caseServices,
alerts: samplePush.alerts,
formatUrl,
});
expect(result).toEqual(sampleServiceRequestData);
});

Expand All @@ -225,7 +264,13 @@ describe('usePostPushToService', () => {
type: ConnectorTypes.jira,
fields: { issueType: 'Task', priority: 'High', parent: 'RJ-01' },
};
const result = formatServiceRequestData(pushedCase, connector as CaseConnector, caseServices);
const result = formatServiceRequestData({
myCase: pushedCase,
connector: connector as CaseConnector,
caseServices,
alerts: samplePush.alerts,
formatUrl,
});
expect(result).toEqual({
...sampleServiceRequestData,
...connector.fields,
Expand All @@ -237,13 +282,22 @@ describe('usePostPushToService', () => {
const caseServices = {
'123': sampleCaseServices['123'],
};

const connector = {
id: '456',
name: 'connector 2',
type: ConnectorTypes.jira,
fields: { issueType: 'Task', priority: 'High', parent: null },
};
const result = formatServiceRequestData(pushedCase, connector as CaseConnector, caseServices);

const result = formatServiceRequestData({
myCase: pushedCase,
connector: connector as CaseConnector,
caseServices,
alerts: samplePush.alerts,
formatUrl,
});

expect(result).toEqual({
...sampleServiceRequestData,
...connector.fields,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,27 @@
*/

import { useReducer, useCallback } from 'react';
import moment from 'moment';
import dateMath from '@elastic/datemath';

import {
ServiceConnectorCaseResponse,
ServiceConnectorCaseParams,
CaseConnector,
CommentType,
} from '../../../../case/common/api';
import { SecurityPageName } from '../../app/types';
import { useFormatUrl, FormatUrl, getRuleDetailsUrl } from '../../common/components/link_to';
import {
errorToToaster,
useStateToaster,
displaySuccessToast,
} from '../../common/components/toasters';
import { Alert } from '../components/case_view';

import { getCase, pushToService, pushCase } from './api';
import * as i18n from './translations';
import { Case } from './types';
import { Case, Comment } from './types';
import { CaseServices } from './use_get_case_user_actions';

interface PushToServiceState {
Expand Down Expand Up @@ -72,6 +77,7 @@ interface PushToServiceRequest {
caseId: string;
connector: CaseConnector;
caseServices: CaseServices;
alerts: Record<string, Alert>;
updateCase: (newCase: Case) => void;
}

Expand All @@ -80,6 +86,7 @@ export interface UsePostPushToService extends PushToServiceState {
caseId,
caseServices,
connector,
alerts,
updateCase,
}: PushToServiceRequest) => void;
}
Expand All @@ -92,9 +99,10 @@ export const usePostPushToService = (): UsePostPushToService => {
isError: false,
});
const [, dispatchToaster] = useStateToaster();
const { formatUrl } = useFormatUrl(SecurityPageName.detections);

const postPushToService = useCallback(
async ({ caseId, caseServices, connector, updateCase }: PushToServiceRequest) => {
async ({ caseId, caseServices, connector, alerts, updateCase }: PushToServiceRequest) => {
let cancel = false;
const abortCtrl = new AbortController();
try {
Expand All @@ -103,7 +111,13 @@ export const usePostPushToService = (): UsePostPushToService => {
const responseService = await pushToService(
connector.id,
connector.type,
formatServiceRequestData(casePushData, connector, caseServices),
formatServiceRequestData({
myCase: casePushData,
connector,
caseServices,
alerts,
formatUrl,
}),
abortCtrl.signal
);
const responseCase = await pushCase(
Expand Down Expand Up @@ -148,11 +162,59 @@ export const usePostPushToService = (): UsePostPushToService => {
return { ...state, postPushToService };
};

export const formatServiceRequestData = (
myCase: Case,
connector: CaseConnector,
caseServices: CaseServices
): ServiceConnectorCaseParams => {
export const determineToAndFrom = (alert: Alert) => {
const ellapsedTimeRule = moment.duration(
moment().diff(dateMath.parse(alert.rule?.from != null ? alert.rule?.from : 'now-0s'))
);

const from = moment(alert['@timestamp'] ?? new Date())
.subtract(ellapsedTimeRule)
.toISOString();
const to = moment(alert['@timestamp'] ?? new Date()).toISOString();

return { to, from };
};

const getAlertFilterUrl = (alert: Alert): string => {
const { to, from } = determineToAndFrom(alert);
return `?filters=!((%27$state%27:(store:appState),meta:(alias:!n,disabled:!f,key:_id,negate:!f,params:(query:${alert._id}),type:phrase),query:(match:(_id:(query:${alert._id},type:phrase)))))&sourcerer=(default:!())&timerange=(global:(linkTo:!(timeline),timerange:(from:%27${from}%27,kind:absolute,to:%27${to}%27)),timeline:(linkTo:!(global),timerange:(from:%27${from}%27,kind:absolute,to:%27${to}%27)))`;
};

const getCommentContent = (
comment: Comment,
alerts: Record<string, Alert>,
formatUrl: FormatUrl
): string => {
if (comment.type === CommentType.user) {
return comment.comment;
} else if (comment.type === CommentType.alert) {
const alert = alerts[comment.alertId];
const ruleDetailsLink = formatUrl(getRuleDetailsUrl(alert.rule.id), {
absolute: true,
skipSearch: true,
});

return `[${i18n.ALERT}](${ruleDetailsLink}${getAlertFilterUrl(alert)}) ${
i18n.ALERT_ADDED_TO_CASE
}`;
}

return '';
};

export const formatServiceRequestData = ({
myCase,
connector,
caseServices,
alerts,
formatUrl,
}: {
myCase: Case;
connector: CaseConnector;
caseServices: CaseServices;
alerts: Record<string, Alert>;
formatUrl: FormatUrl;
}): ServiceConnectorCaseParams => {
const {
id: caseId,
createdAt,
Expand All @@ -179,7 +241,7 @@ export const formatServiceRequestData = (
)
.map((c) => ({
commentId: c.id,
comment: c.type === CommentType.user ? c.comment : '',
comment: getCommentContent(c, alerts, formatUrl),
createdAt: c.createdAt,
createdBy: {
fullName: c.createdBy.fullName ?? null,
Expand Down
11 changes: 11 additions & 0 deletions x-pack/plugins/security_solution/public/cases/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,14 @@ export const SYNC_ALERTS_HELP = i18n.translate(
'Enabling this option will sync the status of alerts in this case with the case status.',
}
);

export const ALERT = i18n.translate('xpack.securitySolution.common.alertLabel', {
defaultMessage: 'Alert',
});

export const ALERT_ADDED_TO_CASE = i18n.translate(
'xpack.securitySolution.common.alertAddedToCase',
{
defaultMessage: 'added to case',
}
);

0 comments on commit 885bbd3

Please sign in to comment.