mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Endpoint] Refresh action and comments on the Case Details view when Isolation actions are created (#103160)
* Expose new Prop from `CaseComponent` that exposes data refresh callbacks * Refresh case actions and comments if isolation was created successfully
This commit is contained in:
parent
648841429c
commit
91fc3cc2b9
13 changed files with 234 additions and 52 deletions
|
@ -23,6 +23,23 @@ export type StatusAllType = typeof StatusAll;
|
|||
|
||||
export type CaseStatusWithAllStatus = CaseStatuses | StatusAllType;
|
||||
|
||||
/**
|
||||
* The type for the `refreshRef` prop (a `React.Ref`) defined by the `CaseViewComponentProps`.
|
||||
*
|
||||
* @example
|
||||
* const refreshRef = useRef<CaseViewRefreshPropInterface>(null);
|
||||
* return <CaseComponent refreshRef={refreshRef} ...otherProps>
|
||||
*/
|
||||
export type CaseViewRefreshPropInterface = null | {
|
||||
/**
|
||||
* Refreshes the all of the user actions/comments in the view's timeline
|
||||
* (note: this also triggers a silent `refreshCase()`)
|
||||
*/
|
||||
refreshUserActionsAndComments: () => Promise<void>;
|
||||
/** Refreshes the Case information only */
|
||||
refreshCase: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type Comment = CommentRequest & {
|
||||
associationType: AssociationType;
|
||||
id: string;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef, MutableRefObject } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import {
|
||||
|
@ -16,11 +16,19 @@ import {
|
|||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CaseStatuses, CaseAttributes, CaseType, Case, CaseConnector, Ecs } from '../../../common';
|
||||
import {
|
||||
CaseStatuses,
|
||||
CaseAttributes,
|
||||
CaseType,
|
||||
Case,
|
||||
CaseConnector,
|
||||
Ecs,
|
||||
CaseViewRefreshPropInterface,
|
||||
} from '../../../common';
|
||||
import { HeaderPage } from '../header_page';
|
||||
import { EditableTitle } from '../header_page/editable_title';
|
||||
import { TagList } from '../tag_list';
|
||||
import { useGetCase } from '../../containers/use_get_case';
|
||||
import { UseGetCase, useGetCase } from '../../containers/use_get_case';
|
||||
import { UserActionTree } from '../user_action_tree';
|
||||
import { UserList } from '../user_list';
|
||||
import { useUpdateCase } from '../../containers/use_update_case';
|
||||
|
@ -42,6 +50,7 @@ import { getConnectorById } from '../utils';
|
|||
import { DoesNotExist } from './does_not_exist';
|
||||
|
||||
const gutterTimeline = '70px'; // seems to be a timeline reference from the original file
|
||||
|
||||
export interface CaseViewComponentProps {
|
||||
allCasesNavigation: CasesNavigation;
|
||||
caseDetailsNavigation: CasesNavigation;
|
||||
|
@ -54,12 +63,18 @@ export interface CaseViewComponentProps {
|
|||
subCaseId?: string;
|
||||
useFetchAlertData: (alertIds: string[]) => [boolean, Record<string, Ecs>];
|
||||
userCanCrud: boolean;
|
||||
/**
|
||||
* A React `Ref` that Exposes data refresh callbacks.
|
||||
* **NOTE**: Do not hold on to the `.current` object, as it could become stale
|
||||
*/
|
||||
refreshRef?: MutableRefObject<CaseViewRefreshPropInterface>;
|
||||
}
|
||||
|
||||
export interface CaseViewProps extends CaseViewComponentProps {
|
||||
onCaseDataSuccess?: (data: Case) => void;
|
||||
timelineIntegration?: CasesTimelineIntegration;
|
||||
}
|
||||
|
||||
export interface OnUpdateFields {
|
||||
key: keyof Case;
|
||||
value: Case[keyof Case];
|
||||
|
@ -78,13 +93,14 @@ const MyEuiFlexGroup = styled(EuiFlexGroup)`
|
|||
|
||||
const MyEuiHorizontalRule = styled(EuiHorizontalRule)`
|
||||
margin-left: 48px;
|
||||
|
||||
&.euiHorizontalRule--full {
|
||||
width: calc(100% - 48px);
|
||||
}
|
||||
`;
|
||||
|
||||
export interface CaseComponentProps extends CaseViewComponentProps {
|
||||
fetchCase: () => void;
|
||||
fetchCase: UseGetCase['fetchCase'];
|
||||
caseData: Case;
|
||||
updateCase: (newCase: Case) => void;
|
||||
}
|
||||
|
@ -105,6 +121,7 @@ export const CaseComponent = React.memo<CaseComponentProps>(
|
|||
updateCase,
|
||||
useFetchAlertData,
|
||||
userCanCrud,
|
||||
refreshRef,
|
||||
}) => {
|
||||
const [initLoadingData, setInitLoadingData] = useState(true);
|
||||
const init = useRef(true);
|
||||
|
@ -124,6 +141,51 @@ export const CaseComponent = React.memo<CaseComponentProps>(
|
|||
subCaseId,
|
||||
});
|
||||
|
||||
// Set `refreshRef` if needed
|
||||
useEffect(() => {
|
||||
let isStale = false;
|
||||
|
||||
if (refreshRef) {
|
||||
refreshRef.current = {
|
||||
refreshCase: async () => {
|
||||
// Do nothing if component (or instance of this render cycle) is stale
|
||||
if (isStale) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchCase();
|
||||
},
|
||||
refreshUserActionsAndComments: async () => {
|
||||
// Do nothing if component (or instance of this render cycle) is stale
|
||||
// -- OR --
|
||||
// it is already loading
|
||||
if (isStale || isLoadingUserActions) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
fetchCase(true),
|
||||
fetchCaseUserActions(caseId, caseData.connector.id, subCaseId),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
return () => {
|
||||
isStale = true;
|
||||
refreshRef.current = null;
|
||||
};
|
||||
}
|
||||
}, [
|
||||
caseData.connector.id,
|
||||
caseId,
|
||||
fetchCase,
|
||||
fetchCaseUserActions,
|
||||
isLoadingUserActions,
|
||||
refreshRef,
|
||||
subCaseId,
|
||||
updateCase,
|
||||
]);
|
||||
|
||||
// Update Fields
|
||||
const onUpdateField = useCallback(
|
||||
({ key, value, onSuccess, onError }: OnUpdateFields) => {
|
||||
|
@ -491,6 +553,7 @@ export const CaseView = React.memo(
|
|||
timelineIntegration,
|
||||
useFetchAlertData,
|
||||
userCanCrud,
|
||||
refreshRef,
|
||||
}: CaseViewProps) => {
|
||||
const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId, subCaseId);
|
||||
if (isError) {
|
||||
|
@ -528,6 +591,7 @@ export const CaseView = React.memo(
|
|||
updateCase={updateCase}
|
||||
useFetchAlertData={useFetchAlertData}
|
||||
userCanCrud={userCanCrud}
|
||||
refreshRef={refreshRef}
|
||||
/>
|
||||
</OwnerProvider>
|
||||
</CasesTimelineIntegrationProvider>
|
||||
|
|
|
@ -89,6 +89,19 @@ describe('useGetCase', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('set isLoading to false when refetching case "silent"ly', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetCase>(() =>
|
||||
useGetCase(basicCase.id)
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
result.current.fetchCase(true);
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('unhappy path', async () => {
|
||||
const spyOnGetCase = jest.spyOn(api, 'getCase');
|
||||
spyOnGetCase.mockImplementation(() => {
|
||||
|
|
|
@ -19,7 +19,7 @@ interface CaseState {
|
|||
}
|
||||
|
||||
type Action =
|
||||
| { type: 'FETCH_INIT' }
|
||||
| { type: 'FETCH_INIT'; payload: { silent: boolean } }
|
||||
| { type: 'FETCH_SUCCESS'; payload: Case }
|
||||
| { type: 'FETCH_FAILURE' }
|
||||
| { type: 'UPDATE_CASE'; payload: Case };
|
||||
|
@ -29,7 +29,10 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => {
|
|||
case 'FETCH_INIT':
|
||||
return {
|
||||
...state,
|
||||
isLoading: true,
|
||||
// If doing a silent fetch, then don't set `isLoading`. This helps
|
||||
// with preventing screen flashing when wanting to refresh the actions
|
||||
// and comments
|
||||
isLoading: !action.payload?.silent,
|
||||
isError: false,
|
||||
};
|
||||
case 'FETCH_SUCCESS':
|
||||
|
@ -56,7 +59,11 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => {
|
|||
};
|
||||
|
||||
export interface UseGetCase extends CaseState {
|
||||
fetchCase: () => void;
|
||||
/**
|
||||
* @param [silent] When set to `true`, the `isLoading` property will not be set to `true`
|
||||
* while doing the API call
|
||||
*/
|
||||
fetchCase: (silent?: boolean) => Promise<void>;
|
||||
updateCase: (newCase: Case) => void;
|
||||
}
|
||||
|
||||
|
@ -74,33 +81,35 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => {
|
|||
dispatch({ type: 'UPDATE_CASE', payload: newCase });
|
||||
}, []);
|
||||
|
||||
const callFetch = useCallback(async () => {
|
||||
try {
|
||||
isCancelledRef.current = false;
|
||||
abortCtrlRef.current.abort();
|
||||
abortCtrlRef.current = new AbortController();
|
||||
dispatch({ type: 'FETCH_INIT' });
|
||||
const callFetch = useCallback(
|
||||
async (silent: boolean = false) => {
|
||||
try {
|
||||
isCancelledRef.current = false;
|
||||
abortCtrlRef.current.abort();
|
||||
abortCtrlRef.current = new AbortController();
|
||||
dispatch({ type: 'FETCH_INIT', payload: { silent } });
|
||||
|
||||
const response = await (subCaseId
|
||||
? getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal)
|
||||
: getCase(caseId, true, abortCtrlRef.current.signal));
|
||||
const response = await (subCaseId
|
||||
? getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal)
|
||||
: getCase(caseId, true, abortCtrlRef.current.signal));
|
||||
|
||||
if (!isCancelledRef.current) {
|
||||
dispatch({ type: 'FETCH_SUCCESS', payload: response });
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isCancelledRef.current) {
|
||||
if (error.name !== 'AbortError') {
|
||||
toasts.addError(
|
||||
error.body && error.body.message ? new Error(error.body.message) : error,
|
||||
{ title: i18n.ERROR_TITLE }
|
||||
);
|
||||
if (!isCancelledRef.current) {
|
||||
dispatch({ type: 'FETCH_SUCCESS', payload: response });
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isCancelledRef.current) {
|
||||
if (error.name !== 'AbortError') {
|
||||
toasts.addError(
|
||||
error.body && error.body.message ? new Error(error.body.message) : error,
|
||||
{ title: i18n.ERROR_TITLE }
|
||||
);
|
||||
}
|
||||
dispatch({ type: 'FETCH_FAILURE' });
|
||||
}
|
||||
dispatch({ type: 'FETCH_FAILURE' });
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [caseId, subCaseId]);
|
||||
},
|
||||
[caseId, subCaseId, toasts]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
callFetch();
|
||||
|
|
|
@ -51,7 +51,11 @@ export const initialData: CaseUserActionsState = {
|
|||
};
|
||||
|
||||
export interface UseGetCaseUserActions extends CaseUserActionsState {
|
||||
fetchCaseUserActions: (caseId: string, caseConnectorId: string, subCaseId?: string) => void;
|
||||
fetchCaseUserActions: (
|
||||
caseId: string,
|
||||
caseConnectorId: string,
|
||||
subCaseId?: string
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
const getExternalService = (value: string): CaseExternalService | null =>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { SearchResponse } from 'elasticsearch';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
@ -19,7 +19,7 @@ import {
|
|||
useFormatUrl,
|
||||
} from '../../../common/components/link_to';
|
||||
import { Ecs } from '../../../../common/ecs';
|
||||
import { Case } from '../../../../../cases/common';
|
||||
import { Case, CaseViewRefreshPropInterface } from '../../../../../cases/common';
|
||||
import { TimelineId } from '../../../../common/types/timeline';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { KibanaServices, useKibana } from '../../../common/lib/kibana';
|
||||
|
@ -38,6 +38,7 @@ import { SEND_ALERT_TO_TIMELINE } from './translations';
|
|||
import { useInsertTimeline } from '../use_insert_timeline';
|
||||
import { SpyRoute } from '../../../common/utils/route/spy_routes';
|
||||
import * as timelineMarkdownPlugin from '../../../common/components/markdown_editor/plugins/timeline';
|
||||
import { CaseDetailsRefreshContext } from '../../../common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context';
|
||||
|
||||
interface Props {
|
||||
caseId: string;
|
||||
|
@ -176,9 +177,13 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
|
|||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
const refreshRef = useRef<CaseViewRefreshPropInterface>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CaseDetailsRefreshContext.Provider value={refreshRef}>
|
||||
{casesUi.getCaseView({
|
||||
refreshRef,
|
||||
allCasesNavigation: {
|
||||
href: formattedAllCasesLink,
|
||||
onClick: async (e) => {
|
||||
|
@ -247,7 +252,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
|
|||
userCanCrud,
|
||||
})}
|
||||
<SpyRoute state={spyState} pageName={SecurityPageName.case} />
|
||||
</>
|
||||
</CaseDetailsRefreshContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 React, { MutableRefObject, useContext } from 'react';
|
||||
import { CaseViewRefreshPropInterface } from '../../../../../../cases/common';
|
||||
|
||||
/**
|
||||
* React Context that can hold the `Ref` that is created an passed to `CaseViewProps['refreshRef`]`, enabling
|
||||
* child components to trigger a refresh of a case.
|
||||
*/
|
||||
export const CaseDetailsRefreshContext = React.createContext<MutableRefObject<CaseViewRefreshPropInterface> | null>(
|
||||
null
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the closes CaseDetails Refresh interface if any. Used in conjuction with `CaseDetailsRefreshContext` component
|
||||
*
|
||||
* @example
|
||||
* // Higher-order component
|
||||
* const refreshRef = useRef<CaseViewRefreshPropInterface>(null);
|
||||
* return <CaseDetailsRefreshContext.Provider value={refreshRef}>....</CaseDetailsRefreshContext.Provider>
|
||||
*
|
||||
* // Now, use the hook from a hild component that was rendered inside of `<CaseDetailsRefreshContext.Provider>`
|
||||
* const caseDetailsRefresh = useWithCaseDetailsRefresh();
|
||||
* ...
|
||||
* if (caseDetailsRefresh) {
|
||||
* caseDetailsRefresh.refreshUserActionsAndComments();
|
||||
* }
|
||||
*/
|
||||
export const useWithCaseDetailsRefresh = (): Readonly<CaseViewRefreshPropInterface> | undefined => {
|
||||
return useContext(CaseDetailsRefreshContext)?.current;
|
||||
};
|
|
@ -20,10 +20,12 @@ export const HostIsolationPanel = React.memo(
|
|||
({
|
||||
details,
|
||||
cancelCallback,
|
||||
successCallback,
|
||||
isolateAction,
|
||||
}: {
|
||||
details: Maybe<TimelineEventsDetailsItem[]>;
|
||||
cancelCallback: () => void;
|
||||
successCallback?: () => void;
|
||||
isolateAction: string;
|
||||
}) => {
|
||||
const endpointId = useMemo(() => {
|
||||
|
@ -92,6 +94,7 @@ export const HostIsolationPanel = React.memo(
|
|||
cases={associatedCases}
|
||||
casesInfo={casesInfo}
|
||||
cancelCallback={cancelCallback}
|
||||
successCallback={successCallback}
|
||||
/>
|
||||
) : (
|
||||
<UnisolateHost
|
||||
|
@ -100,6 +103,7 @@ export const HostIsolationPanel = React.memo(
|
|||
cases={associatedCases}
|
||||
casesInfo={casesInfo}
|
||||
cancelCallback={cancelCallback}
|
||||
successCallback={successCallback}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,12 +24,14 @@ export const IsolateHost = React.memo(
|
|||
cases,
|
||||
casesInfo,
|
||||
cancelCallback,
|
||||
successCallback,
|
||||
}: {
|
||||
endpointId: string;
|
||||
hostName: string;
|
||||
cases: ReactNode;
|
||||
casesInfo: CasesFromAlertsResponse;
|
||||
cancelCallback: () => void;
|
||||
successCallback?: () => void;
|
||||
}) => {
|
||||
const [comment, setComment] = useState('');
|
||||
const [isIsolated, setIsIsolated] = useState(false);
|
||||
|
@ -43,7 +45,11 @@ export const IsolateHost = React.memo(
|
|||
const confirmHostIsolation = useCallback(async () => {
|
||||
const hostIsolated = await isolateHost();
|
||||
setIsIsolated(hostIsolated);
|
||||
}, [isolateHost]);
|
||||
|
||||
if (hostIsolated && successCallback) {
|
||||
successCallback();
|
||||
}
|
||||
}, [isolateHost, successCallback]);
|
||||
|
||||
const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]);
|
||||
|
||||
|
|
|
@ -24,12 +24,14 @@ export const UnisolateHost = React.memo(
|
|||
cases,
|
||||
casesInfo,
|
||||
cancelCallback,
|
||||
successCallback,
|
||||
}: {
|
||||
endpointId: string;
|
||||
hostName: string;
|
||||
cases: ReactNode;
|
||||
casesInfo: CasesFromAlertsResponse;
|
||||
cancelCallback: () => void;
|
||||
successCallback?: () => void;
|
||||
}) => {
|
||||
const [comment, setComment] = useState('');
|
||||
const [isUnIsolated, setIsUnIsolated] = useState(false);
|
||||
|
@ -41,9 +43,13 @@ export const UnisolateHost = React.memo(
|
|||
const { loading, unIsolateHost } = useHostUnisolation({ endpointId, comment, caseIds });
|
||||
|
||||
const confirmHostUnIsolation = useCallback(async () => {
|
||||
const hostIsolated = await unIsolateHost();
|
||||
setIsUnIsolated(hostIsolated);
|
||||
}, [unIsolateHost]);
|
||||
const hostUnIsolated = await unIsolateHost();
|
||||
setIsUnIsolated(hostUnIsolated);
|
||||
|
||||
if (hostUnIsolated && successCallback) {
|
||||
successCallback();
|
||||
}
|
||||
}, [successCallback, unIsolateHost]);
|
||||
|
||||
const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]);
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { createHostIsolation } from './api';
|
|||
|
||||
interface HostIsolationStatus {
|
||||
loading: boolean;
|
||||
/** Boolean return will indicate if isolation action was created successful */
|
||||
isolateHost: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
import { ALERT_DETAILS } from './translations';
|
||||
import { useIsolationPrivileges } from '../../../../common/hooks/endpoint/use_isolate_privileges';
|
||||
import { endpointAlertCheck } from '../../../../common/utils/endpoint_alert_check';
|
||||
import { useWithCaseDetailsRefresh } from '../../../../common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context';
|
||||
|
||||
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
|
||||
.euiFlyoutBody__overflow {
|
||||
|
@ -121,6 +122,15 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
|
|||
);
|
||||
}, [showAlertDetails, isolateAction]);
|
||||
|
||||
const caseDetailsRefresh = useWithCaseDetailsRefresh();
|
||||
|
||||
const handleIsolationActionSuccess = useCallback(() => {
|
||||
// If a case details refresh ref is defined, then refresh actions and comments
|
||||
if (caseDetailsRefresh) {
|
||||
caseDetailsRefresh.refreshUserActionsAndComments();
|
||||
}
|
||||
}, [caseDetailsRefresh]);
|
||||
|
||||
if (!expandedEvent?.eventId) {
|
||||
return null;
|
||||
}
|
||||
|
@ -139,6 +149,7 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
|
|||
<HostIsolationPanel
|
||||
details={detailsData}
|
||||
cancelCallback={showAlertDetails}
|
||||
successCallback={handleIsolationActionSuccess}
|
||||
isolateAction={isolateAction}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
@ -61,6 +61,7 @@ export const isolationRequestHandler = function (
|
|||
TypeOf<typeof HostIsolationRequestSchema.body>,
|
||||
SecuritySolutionRequestHandlerContext
|
||||
> {
|
||||
// eslint-disable-next-line complexity
|
||||
return async (context, req, res) => {
|
||||
if (
|
||||
(!req.body.agent_ids || req.body.agent_ids.length === 0) &&
|
||||
|
@ -100,14 +101,14 @@ export const isolationRequestHandler = function (
|
|||
}
|
||||
agentIDs = [...new Set(agentIDs)]; // dedupe
|
||||
|
||||
const casesClient = await endpointContext.service.getCasesClient(req);
|
||||
|
||||
// convert any alert IDs into cases
|
||||
let caseIDs: string[] = req.body.case_ids?.slice() || [];
|
||||
if (req.body.alert_ids && req.body.alert_ids.length > 0) {
|
||||
const newIDs: string[][] = await Promise.all(
|
||||
req.body.alert_ids.map(async (a: string) => {
|
||||
const cases: CasesByAlertId = await (
|
||||
await endpointContext.service.getCasesClient(req)
|
||||
).cases.getCasesByAlertID({
|
||||
const cases: CasesByAlertId = await casesClient.cases.getCasesByAlertID({
|
||||
alertID: a,
|
||||
options: { owner: APP_ID },
|
||||
});
|
||||
|
@ -167,16 +168,21 @@ export const isolationRequestHandler = function (
|
|||
commentLines.push(`\n\nWith Comment:\n> ${req.body.comment}`);
|
||||
}
|
||||
|
||||
caseIDs.forEach(async (caseId) => {
|
||||
(await endpointContext.service.getCasesClient(req)).attachments.add({
|
||||
caseId,
|
||||
comment: {
|
||||
comment: commentLines.join('\n'),
|
||||
type: CommentType.user,
|
||||
owner: APP_ID,
|
||||
},
|
||||
});
|
||||
});
|
||||
// Update all cases with a comment
|
||||
if (caseIDs.length > 0) {
|
||||
await Promise.all(
|
||||
caseIDs.map((caseId) =>
|
||||
casesClient.attachments.add({
|
||||
caseId,
|
||||
comment: {
|
||||
comment: commentLines.join('\n'),
|
||||
type: CommentType.user,
|
||||
owner: APP_ID,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return res.ok({
|
||||
body: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue