From 833e13faff59b99f24cd793b932081d6cfc9d879 Mon Sep 17 00:00:00 2001
From: Candace Park <56409205+parkiino@users.noreply.github.com>
Date: Thu, 13 May 2021 09:37:56 -0400
Subject: [PATCH] [Security Solution][Endpoint][Host Isolation] Send case ids
from UI to isolate api (#99484)
---
x-pack/plugins/cases/common/api/helpers.ts | 5 ++
.../components/host_isolation/index.tsx | 58 ++++++++++++++-----
.../components/host_isolation/translations.ts | 14 +++--
.../detection_engine/alerts/__mocks__/api.ts | 17 +++++-
.../detection_engine/alerts/api.test.ts | 31 ++++++++++
.../containers/detection_engine/alerts/api.ts | 20 +++++++
.../detection_engine/alerts/mock.ts | 12 +++-
.../detection_engine/alerts/translations.ts | 5 ++
.../detection_engine/alerts/types.ts | 2 +
.../alerts/use_cases_from_alerts.test.tsx | 41 +++++++++++++
.../alerts/use_cases_from_alerts.tsx | 51 ++++++++++++++++
.../alerts/use_host_isolation.tsx | 6 +-
12 files changed, 235 insertions(+), 27 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx
diff --git a/x-pack/plugins/cases/common/api/helpers.ts b/x-pack/plugins/cases/common/api/helpers.ts
index 43e292b91db4b..7ac686ce5c8dd 100644
--- a/x-pack/plugins/cases/common/api/helpers.ts
+++ b/x-pack/plugins/cases/common/api/helpers.ts
@@ -14,6 +14,7 @@ import {
SUB_CASES_URL,
CASE_PUSH_URL,
SUB_CASE_USER_ACTIONS_URL,
+ CASE_ALERTS_URL,
} from '../constants';
export const getCaseDetailsUrl = (id: string): string => {
@@ -47,3 +48,7 @@ export const getSubCaseUserActionUrl = (caseID: string, subCaseId: string): stri
export const getCasePushUrl = (caseId: string, connectorId: string): string => {
return CASE_PUSH_URL.replace('{case_id}', caseId).replace('{connector_id}', connectorId);
};
+
+export const getCasesFromAlertsUrl = (alertId: string): string => {
+ return CASE_ALERTS_URL.replace('{alert_id}', alertId);
+};
diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx
index 30ee7e77f3a7d..3897458e8459c 100644
--- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx
@@ -21,7 +21,6 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { useHostIsolation } from '../../containers/detection_engine/alerts/use_host_isolation';
-import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
import {
CANCEL,
CASES_ASSOCIATED_WITH_ALERT,
@@ -31,6 +30,9 @@ import {
RETURN_TO_ALERT_DETAILS,
} from './translations';
import { Maybe } from '../../../../../observability/common/typings';
+import { useCasesFromAlerts } from '../../containers/detection_engine/alerts/use_cases_from_alerts';
+import { CaseDetailsLink } from '../../../common/components/links';
+import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
export const HostIsolationPanel = React.memo(
({
@@ -59,7 +61,13 @@ export const HostIsolationPanel = React.memo(
return findAlertRule ? findAlertRule[0] : '';
}, [details]);
- const { loading, isolateHost } = useHostIsolation({ agentId, comment });
+ const alertId = useMemo(() => {
+ const findAlertId = find({ category: '_id', field: '_id' }, details)?.values;
+ return findAlertId ? findAlertId[0] : '';
+ }, [details]);
+
+ const { caseIds } = useCasesFromAlerts({ alertId });
+ const { loading, isolateHost } = useHostIsolation({ agentId, comment, caseIds });
const confirmHostIsolation = useCallback(async () => {
const hostIsolated = await isolateHost();
@@ -68,8 +76,25 @@ export const HostIsolationPanel = React.memo(
const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]);
- // a placeholder until we get the case count returned from a new case route in a future pr
- const caseCount: number = 0;
+ const casesList = useMemo(
+ () =>
+ caseIds.map((id, index) => {
+ return (
+
+
+
+
+
+ );
+ }),
+ [caseIds]
+ );
+
+ const caseCount: number = useMemo(() => caseIds.length, [caseIds]);
const hostIsolated = useMemo(() => {
return (
@@ -92,20 +117,13 @@ export const HostIsolationPanel = React.memo(
-
+
>
)}
@@ -121,7 +139,7 @@ export const HostIsolationPanel = React.memo(
>
);
- }, [backToAlertDetails, hostName]);
+ }, [backToAlertDetails, hostName, caseCount, casesList]);
const hostNotIsolated = useMemo(() => {
return (
@@ -137,7 +155,7 @@ export const HostIsolationPanel = React.memo(
cases: (
{caseCount}
- {CASES_ASSOCIATED_WITH_ALERT}
+ {CASES_ASSOCIATED_WITH_ALERT(caseCount)}
{alertRule}
),
@@ -171,7 +189,15 @@ export const HostIsolationPanel = React.memo(
>
);
- }, [alertRule, backToAlertDetails, comment, confirmHostIsolation, hostName, loading]);
+ }, [
+ alertRule,
+ backToAlertDetails,
+ comment,
+ confirmHostIsolation,
+ hostName,
+ loading,
+ caseCount,
+ ]);
return isIsolated ? hostIsolated : hostNotIsolated;
}
diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts b/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts
index 97a1a278952a6..8d6334f6c340d 100644
--- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts
@@ -31,12 +31,14 @@ export const CONFIRM = i18n.translate('xpack.securitySolution.endpoint.hostIsola
defaultMessage: 'Confirm',
});
-export const CASES_ASSOCIATED_WITH_ALERT = i18n.translate(
- 'xpack.securitySolution.endpoint.hostIsolation.isolateHost.casesAssociatedWihtAlert',
- {
- defaultMessage: ' cases associated with the rule ',
- }
-);
+export const CASES_ASSOCIATED_WITH_ALERT = (caseCount: number): string =>
+ i18n.translate(
+ 'xpack.securitySolution.endpoint.hostIsolation.isolateHost.casesAssociatedWithAlert',
+ {
+ defaultMessage: ' {caseCount, plural, one {case} other {cases}} associated with the rule ',
+ values: { caseCount },
+ }
+ );
export const RETURN_TO_ALERT_DETAILS = i18n.translate(
'xpack.securitySolution.endpoint.hostIsolation.returnToAlertDetails',
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/__mocks__/api.ts
index e1f5b53e2f4c3..ea64f39226cd2 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/__mocks__/api.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/__mocks__/api.ts
@@ -5,8 +5,15 @@
* 2.0.
*/
-import { QueryAlerts, AlertSearchResponse, BasicSignals, AlertsIndex, Privilege } from '../types';
-import { alertsMock, mockSignalIndex, mockUserPrivilege } from '../mock';
+import {
+ QueryAlerts,
+ AlertSearchResponse,
+ BasicSignals,
+ AlertsIndex,
+ Privilege,
+ CasesFromAlertsResponse,
+} from '../types';
+import { alertsMock, mockSignalIndex, mockUserPrivilege, mockCaseIdsFromAlertId } from '../mock';
export const fetchQueryAlerts = async ({
query,
@@ -22,3 +29,9 @@ export const getUserPrivilege = async ({ signal }: BasicSignals): Promise =>
Promise.resolve(mockSignalIndex);
+
+export const getCaseIdsFromAlertId = async ({
+ alertId,
+}: {
+ alertId: string;
+}): Promise => Promise.resolve(mockCaseIdsFromAlertId);
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts
index 82f275f7dc9ba..9aa5cfd229292 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.test.ts
@@ -12,6 +12,7 @@ import {
mockStatusAlertQuery,
mockSignalIndex,
mockUserPrivilege,
+ mockHostIsolation,
} from './mock';
import {
fetchQueryAlerts,
@@ -19,6 +20,7 @@ import {
getSignalIndex,
getUserPrivilege,
createSignalIndex,
+ createHostIsolation,
} from './api';
const abortCtrl = new AbortController();
@@ -163,4 +165,33 @@ describe('Detections Alerts API', () => {
expect(alertsResp).toEqual(mockSignalIndex);
});
});
+
+ describe('createHostIsolation', () => {
+ beforeEach(() => {
+ fetchMock.mockClear();
+ fetchMock.mockResolvedValue(mockHostIsolation);
+ });
+
+ test('check parameter url', async () => {
+ await createHostIsolation({
+ agentId: 'fd8a122b-4c54-4c05-b295-e5f8381fc59d',
+ comment: 'commento',
+ caseIds: ['88c04a90-b19c-11eb-b838-bf3c7840b969'],
+ });
+ expect(fetchMock).toHaveBeenCalledWith('/api/endpoint/isolate', {
+ method: 'POST',
+ body:
+ '{"agent_ids":["fd8a122b-4c54-4c05-b295-e5f8381fc59d"],"comment":"commento","case_ids":["88c04a90-b19c-11eb-b838-bf3c7840b969"]}',
+ });
+ });
+
+ test('happy path', async () => {
+ const hostIsolationResponse = await createHostIsolation({
+ agentId: 'fd8a122b-4c54-4c05-b295-e5f8381fc59d',
+ comment: 'commento',
+ caseIds: ['88c04a90-b19c-11eb-b838-bf3c7840b969'],
+ });
+ expect(hostIsolationResponse).toEqual(mockHostIsolation);
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts
index dbcb11383432f..300005b23caaa 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts
@@ -6,6 +6,7 @@
*/
import { UpdateDocumentByQueryResponse } from 'elasticsearch';
+import { getCasesFromAlertsUrl } from '../../../../../../cases/common';
import { HostIsolationResponse } from '../../../../../common/endpoint/types';
import {
DETECTION_ENGINE_QUERY_SIGNALS_URL,
@@ -22,6 +23,7 @@ import {
AlertSearchResponse,
AlertsIndex,
UpdateAlertStatusProps,
+ CasesFromAlertsResponse,
} from './types';
/**
@@ -109,20 +111,38 @@ export const createSignalIndex = async ({ signal }: BasicSignals): Promise =>
KibanaServices.get().http.fetch(ISOLATE_HOST_ROUTE, {
method: 'POST',
body: JSON.stringify({
agent_ids: [agentId],
comment,
+ case_ids: caseIds,
}),
});
+
+/**
+ * Get list of associated case ids from alert id
+ *
+ * @param alert id
+ */
+export const getCaseIdsFromAlertId = async ({
+ alertId,
+}: {
+ alertId: string;
+}): Promise =>
+ KibanaServices.get().http.fetch(getCasesFromAlertsUrl(alertId), {
+ method: 'get',
+ });
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
index 18651063df8ca..69358958a395c 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
@@ -5,7 +5,8 @@
* 2.0.
*/
-import { AlertSearchResponse, AlertsIndex, Privilege } from './types';
+import { HostIsolationResponse } from '../../../../../common/endpoint/types/actions';
+import { AlertSearchResponse, AlertsIndex, Privilege, CasesFromAlertsResponse } from './types';
export const alertsMock: AlertSearchResponse = {
took: 7,
@@ -1039,3 +1040,12 @@ export const mockUserPrivilege: Privilege = {
is_authenticated: true,
has_encryption_key: true,
};
+
+export const mockHostIsolation: HostIsolationResponse = {
+ action: '713085d6-ab45-4e9e-b41d-96563cafdd97',
+};
+
+export const mockCaseIdsFromAlertId: CasesFromAlertsResponse = [
+ '818601a0-b26b-11eb-8759-6b318e8cf4bc',
+ '8a774850-b26b-11eb-8759-6b318e8cf4bc',
+];
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts
index 2998c97376c26..ed6a22375a776 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts
@@ -32,3 +32,8 @@ export const HOST_ISOLATION_FAILURE = i18n.translate(
'xpack.securitySolution.endpoint.hostIsolation.failedToIsolate.title',
{ defaultMessage: 'Failed to isolate host' }
);
+
+export const CASES_FROM_ALERTS_FAILURE = i18n.translate(
+ 'xpack.securitySolution.endpoint.hostIsolation.casesFromAlerts.title',
+ { defaultMessage: 'Failed to find associated cases' }
+);
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
index 26108ca939a57..52b477d95076b 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts
@@ -48,6 +48,8 @@ export interface AlertsIndex {
index_mapping_outdated: boolean;
}
+export type CasesFromAlertsResponse = string[];
+
export interface Privilege {
username: string;
has_all_requested: boolean;
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx
new file mode 100644
index 0000000000000..0867fb001051a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.test.tsx
@@ -0,0 +1,41 @@
+/*
+ * 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 { renderHook } from '@testing-library/react-hooks';
+import { useCasesFromAlerts } from './use_cases_from_alerts';
+import * as api from './api';
+import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
+import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
+import { mockCaseIdsFromAlertId } from './mock';
+
+jest.mock('./api');
+jest.mock('../../../../common/hooks/use_app_toasts');
+
+describe('useCasesFromAlerts hook', () => {
+ let appToastsMock: jest.Mocked>;
+ beforeEach(() => {
+ jest.resetAllMocks();
+ appToastsMock = useAppToastsMock.create();
+ (useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
+ });
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('returns an array of caseIds', async () => {
+ const spyOnCases = jest.spyOn(api, 'getCaseIdsFromAlertId');
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useCasesFromAlerts({ alertId: 'anAlertId' })
+ );
+ await waitForNextUpdate();
+ expect(spyOnCases).toHaveBeenCalledTimes(1);
+ expect(result.current).toEqual({
+ loading: false,
+ caseIds: mockCaseIdsFromAlertId,
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx
new file mode 100644
index 0000000000000..fb130eb744700
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx
@@ -0,0 +1,51 @@
+/*
+ * 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 { isEmpty } from 'lodash';
+import { useEffect, useState } from 'react';
+import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
+import { getCaseIdsFromAlertId } from './api';
+import { CASES_FROM_ALERTS_FAILURE } from './translations';
+import { CasesFromAlertsResponse } from './types';
+
+interface CasesFromAlertsStatus {
+ loading: boolean;
+ caseIds: CasesFromAlertsResponse;
+}
+
+export const useCasesFromAlerts = ({ alertId }: { alertId: string }): CasesFromAlertsStatus => {
+ const [loading, setLoading] = useState(false);
+ const [cases, setCases] = useState([]);
+ const { addError } = useAppToasts();
+
+ useEffect(() => {
+ // isMounted tracks if a component is mounted before changing state
+ let isMounted = true;
+ setLoading(true);
+ const fetchData = async () => {
+ try {
+ const casesResponse = await getCaseIdsFromAlertId({ alertId });
+ if (isMounted) {
+ setCases(casesResponse);
+ }
+ } catch (error) {
+ addError(error.message, { title: CASES_FROM_ALERTS_FAILURE });
+ }
+ if (isMounted) {
+ setLoading(false);
+ }
+ };
+ if (!isEmpty(alertId)) {
+ fetchData();
+ }
+ return () => {
+ // updates to show component is unmounted
+ isMounted = false;
+ };
+ }, [alertId, addError]);
+ return { loading, caseIds: cases };
+};
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx
index 684bc6af5d2c7..ad3c6e91c03fe 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation.tsx
@@ -18,11 +18,13 @@ interface HostIsolationStatus {
interface UseHostIsolationProps {
agentId: string;
comment: string;
+ caseIds?: string[];
}
export const useHostIsolation = ({
agentId,
comment,
+ caseIds,
}: UseHostIsolationProps): HostIsolationStatus => {
const [loading, setLoading] = useState(false);
const { addError } = useAppToasts();
@@ -30,7 +32,7 @@ export const useHostIsolation = ({
const isolateHost = useCallback(async () => {
try {
setLoading(true);
- const isolationStatus = await createHostIsolation({ agentId, comment });
+ const isolationStatus = await createHostIsolation({ agentId, comment, caseIds });
setLoading(false);
return isolationStatus.action ? true : false;
} catch (error) {
@@ -38,6 +40,6 @@ export const useHostIsolation = ({
addError(error.message, { title: HOST_ISOLATION_FAILURE });
return false;
}
- }, [agentId, comment, addError]);
+ }, [agentId, comment, caseIds, addError]);
return { loading, isolateHost };
};