mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Add more actions to alerts flyout (#105767)
* add investigate in timeline action to flyout * close context menu on item clicked * add investigate in timeline * add investigat in timeline button * fix failing tests * add alerts status actions * update unit test * export alerts actions from hook * add disable props * add case action items * clean up * split alert status hook and hide add to case action * add useHoseIsolationAction hook * move out take action dropdown * refeactor hooks to only manage one thing * apply hooks to alerts table * clean up * fix unit tests * replace euiCodeBlock * take actions from case * fetch ecs in flyout footer * move fetch alert ecs to container * add AddExceptionModalWrapperData interface * fix cypress tests * update snapshot for json view * fix cypress test * update AddEndpointExceptionComponent * fix data retrieved from event details * fix host isolation action * use endpointAlertCheck Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
This commit is contained in:
parent
e69d57cf77
commit
60f8da452f
40 changed files with 1723 additions and 713 deletions
|
@ -469,6 +469,7 @@ export type TimelineExpandedEventType =
|
|||
params?: {
|
||||
eventId: string;
|
||||
indexName: string;
|
||||
refetch?: () => void;
|
||||
};
|
||||
}
|
||||
| EmptyObject;
|
||||
|
|
|
@ -47,7 +47,8 @@ describe('Alert details with unmapped fields', () => {
|
|||
const length = elements.length;
|
||||
cy.wrap(elements)
|
||||
.eq(length - expectedUnmappedField.line)
|
||||
.should('have.text', expectedUnmappedField.text);
|
||||
.invoke('text')
|
||||
.should('include', expectedUnmappedField.text);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -89,7 +89,8 @@ describe('CTI Enrichment', () => {
|
|||
expectedEnrichment.forEach((enrichment) => {
|
||||
cy.wrap(elements)
|
||||
.eq(length - enrichment.line)
|
||||
.should('have.text', enrichment.text);
|
||||
.invoke('text')
|
||||
.should('include', enrichment.text);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -238,10 +238,9 @@ describe('Custom detection rules deletion and edition', () => {
|
|||
goToManageAlertsDetectionRules();
|
||||
waitForAlertsIndexToBeCreated();
|
||||
createCustomRuleActivated(getNewRule(), 'rule1');
|
||||
createCustomRuleActivated(getNewRule(), 'rule2');
|
||||
|
||||
createCustomRuleActivated(getNewOverrideRule(), 'rule3');
|
||||
createCustomRuleActivated(getExistingRule(), 'rule4');
|
||||
createCustomRuleActivated(getNewOverrideRule(), 'rule2');
|
||||
createCustomRuleActivated(getExistingRule(), 'rule3');
|
||||
reload();
|
||||
});
|
||||
|
||||
|
@ -295,7 +294,7 @@ describe('Custom detection rules deletion and edition', () => {
|
|||
});
|
||||
cy.get(SHOWING_RULES_TEXT).should(
|
||||
'have.text',
|
||||
`Showing ${expectedNumberOfRulesAfterDeletion} rules`
|
||||
`Showing ${expectedNumberOfRulesAfterDeletion} rule`
|
||||
);
|
||||
cy.get(CUSTOM_RULES_BTN).should(
|
||||
'have.text',
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const ADD_EXCEPTION_BTN = '[data-test-subj="addExceptionButton"]';
|
||||
export const ADD_EXCEPTION_BTN = '[data-test-subj="add-exception-menu-item"]';
|
||||
|
||||
export const ALERTS = '[data-test-subj="events-viewer-panel"] [data-test-subj="event"]';
|
||||
|
||||
|
|
|
@ -9,9 +9,11 @@ export const ALERT_FLYOUT = '[data-test-subj="timeline:details-panel:flyout"]';
|
|||
|
||||
export const CELL_TEXT = '.euiText';
|
||||
|
||||
export const JSON_VIEW_WRAPPER = '[data-test-subj="jsonViewWrapper"]';
|
||||
|
||||
export const JSON_CONTENT = '[data-test-subj="jsonView"]';
|
||||
|
||||
export const JSON_LINES = '.ace_line';
|
||||
export const JSON_LINES = '.euiCodeBlock__line';
|
||||
|
||||
export const JSON_VIEW_TAB = '[data-test-subj="jsonViewTab"]';
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import {
|
||||
ENRICHMENT_COUNT_NOTIFICATION,
|
||||
JSON_CONTENT,
|
||||
JSON_VIEW_WRAPPER,
|
||||
JSON_VIEW_TAB,
|
||||
TABLE_TAB,
|
||||
} from '../screens/alerts_details';
|
||||
|
@ -25,7 +25,7 @@ export const openThreatIndicatorDetails = () => {
|
|||
};
|
||||
|
||||
export const scrollJsonViewToBottom = () => {
|
||||
cy.get(JSON_CONTENT).click({ force: true });
|
||||
cy.get(JSON_CONTENT).type('{pagedown}{pagedown}{pagedown}');
|
||||
cy.get(JSON_CONTENT).should('be.visible');
|
||||
cy.get(JSON_VIEW_WRAPPER).click({ force: true });
|
||||
cy.get(JSON_VIEW_WRAPPER).type('{pagedown}{pagedown}{pagedown}');
|
||||
cy.get(JSON_VIEW_WRAPPER).should('be.visible');
|
||||
};
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { SearchResponse } from 'elasticsearch';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import {
|
||||
getCaseDetailsUrl,
|
||||
|
@ -18,18 +16,17 @@ import {
|
|||
getRuleDetailsUrl,
|
||||
useFormatUrl,
|
||||
} from '../../../common/components/link_to';
|
||||
import { Ecs } from '../../../../common/ecs';
|
||||
import { Case, CaseViewRefreshPropInterface } from '../../../../../cases/common';
|
||||
import { TimelineId } from '../../../../common/types/timeline';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { KibanaServices, useKibana } from '../../../common/lib/kibana';
|
||||
import { APP_ID, DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../common/constants';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { APP_ID } from '../../../../common/constants';
|
||||
import { timelineActions } from '../../../timelines/store/timeline';
|
||||
import { useSourcererScope } from '../../../common/containers/sourcerer';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { DetailsPanel } from '../../../timelines/components/side_panel';
|
||||
import { InvestigateInTimelineAction } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action';
|
||||
import { buildAlertsQuery, formatAlertToEcsSignal, useFetchAlertData } from './helpers';
|
||||
import { useFetchAlertData } from './helpers';
|
||||
import { SEND_ALERT_TO_TIMELINE } from './translations';
|
||||
import { useInsertTimeline } from '../use_insert_timeline';
|
||||
import { SpyRoute } from '../../../common/utils/route/spy_routes';
|
||||
|
@ -70,39 +67,12 @@ const TimelineDetailsPanel = () => {
|
|||
};
|
||||
|
||||
const InvestigateInTimelineActionComponent = (alertIds: string[]) => {
|
||||
const fetchEcsAlertsData = async (fetchAlertIds?: string[]): Promise<Ecs[]> => {
|
||||
if (isEmpty(fetchAlertIds)) {
|
||||
return [];
|
||||
}
|
||||
const alertResponse = await KibanaServices.get().http.fetch<
|
||||
SearchResponse<{ '@timestamp': string; [key: string]: unknown }>
|
||||
>(DETECTION_ENGINE_QUERY_SIGNALS_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(buildAlertsQuery(fetchAlertIds ?? [])),
|
||||
});
|
||||
return (
|
||||
alertResponse?.hits.hits.reduce<Ecs[]>(
|
||||
(acc, { _id, _index, _source }) => [
|
||||
...acc,
|
||||
{
|
||||
...formatAlertToEcsSignal(_source as {}),
|
||||
_id,
|
||||
_index,
|
||||
timestamp: _source['@timestamp'],
|
||||
},
|
||||
],
|
||||
[]
|
||||
) ?? []
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<InvestigateInTimelineAction
|
||||
ariaLabel={SEND_ALERT_TO_TIMELINE}
|
||||
alertIds={alertIds}
|
||||
key="investigate-in-timeline"
|
||||
ecsRowData={null}
|
||||
fetchEcsAlertsData={fetchEcsAlertsData}
|
||||
nonEcsRowData={[]}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -2,54 +2,50 @@
|
|||
|
||||
exports[`JSON View rendering should match snapshot 1`] = `
|
||||
<styled.div>
|
||||
<EuiCodeEditor
|
||||
<EuiCodeBlock
|
||||
data-test-subj="jsonView"
|
||||
height="100%"
|
||||
isReadOnly={true}
|
||||
mode="javascript"
|
||||
setOptions={
|
||||
Object {
|
||||
"fontSize": "12px",
|
||||
}
|
||||
}
|
||||
value="{
|
||||
\\"_id\\": \\"pEMaMmkBUV60JmNWmWVi\\",
|
||||
\\"_index\\": \\"filebeat-8.0.0-2019.02.19-000001\\",
|
||||
\\"_score\\": 1,
|
||||
\\"_type\\": \\"_doc\\",
|
||||
\\"@timestamp\\": \\"2019-02-28T16:50:54.621Z\\",
|
||||
\\"agent\\": {
|
||||
\\"ephemeral_id\\": \\"9d391ef2-a734-4787-8891-67031178c641\\",
|
||||
\\"hostname\\": \\"siem-kibana\\",
|
||||
\\"id\\": \\"5de03d5f-52f3-482e-91d4-853c7de073c3\\",
|
||||
\\"type\\": \\"filebeat\\",
|
||||
\\"version\\": \\"8.0.0\\"
|
||||
fontSize="m"
|
||||
isCopyable={true}
|
||||
language="json"
|
||||
paddingSize="m"
|
||||
>
|
||||
{
|
||||
"_id": "pEMaMmkBUV60JmNWmWVi",
|
||||
"_index": "filebeat-8.0.0-2019.02.19-000001",
|
||||
"_score": 1,
|
||||
"_type": "_doc",
|
||||
"@timestamp": "2019-02-28T16:50:54.621Z",
|
||||
"agent": {
|
||||
"ephemeral_id": "9d391ef2-a734-4787-8891-67031178c641",
|
||||
"hostname": "siem-kibana",
|
||||
"id": "5de03d5f-52f3-482e-91d4-853c7de073c3",
|
||||
"type": "filebeat",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
\\"cloud\\": {
|
||||
\\"availability_zone\\": \\"projects/189716325846/zones/us-east1-b\\",
|
||||
\\"instance\\": {
|
||||
\\"id\\": \\"5412578377715150143\\",
|
||||
\\"name\\": \\"siem-kibana\\"
|
||||
"cloud": {
|
||||
"availability_zone": "projects/189716325846/zones/us-east1-b",
|
||||
"instance": {
|
||||
"id": "5412578377715150143",
|
||||
"name": "siem-kibana"
|
||||
},
|
||||
\\"machine\\": {
|
||||
\\"type\\": \\"projects/189716325846/machineTypes/n1-standard-1\\"
|
||||
"machine": {
|
||||
"type": "projects/189716325846/machineTypes/n1-standard-1"
|
||||
},
|
||||
\\"project\\": {
|
||||
\\"id\\": \\"elastic-beats\\"
|
||||
"project": {
|
||||
"id": "elastic-beats"
|
||||
},
|
||||
\\"provider\\": \\"gce\\"
|
||||
"provider": "gce"
|
||||
},
|
||||
\\"destination\\": {
|
||||
\\"bytes\\": 584,
|
||||
\\"ip\\": \\"10.47.8.200\\",
|
||||
\\"packets\\": 4,
|
||||
\\"port\\": 902
|
||||
"destination": {
|
||||
"bytes": 584,
|
||||
"ip": "10.47.8.200",
|
||||
"packets": 4,
|
||||
"port": 902
|
||||
},
|
||||
\\"event\\": {
|
||||
\\"kind\\": \\"event\\"
|
||||
"event": {
|
||||
"kind": "event"
|
||||
}
|
||||
}"
|
||||
width="100%"
|
||||
/>
|
||||
}
|
||||
</EuiCodeBlock>
|
||||
</styled.div>
|
||||
`;
|
||||
|
|
|
@ -245,7 +245,7 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
content: (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<TabContentWrapper>
|
||||
<TabContentWrapper data-test-subj="jsonViewWrapper">
|
||||
<JsonView data={data} />
|
||||
</TabContentWrapper>
|
||||
</>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiCodeEditor } from '@elastic/eui';
|
||||
import { EuiCodeBlock } from '@elastic/eui';
|
||||
import { set } from '@elastic/safer-lodash-set/fp';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
@ -23,8 +23,6 @@ const EuiCodeEditorContainer = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
const EDITOR_SET_OPTIONS = { fontSize: '12px' };
|
||||
|
||||
export const JsonView = React.memo<Props>(({ data }) => {
|
||||
const value = useMemo(
|
||||
() =>
|
||||
|
@ -38,15 +36,15 @@ export const JsonView = React.memo<Props>(({ data }) => {
|
|||
|
||||
return (
|
||||
<EuiCodeEditorContainer>
|
||||
<EuiCodeEditor
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
fontSize="m"
|
||||
paddingSize="m"
|
||||
isCopyable
|
||||
data-test-subj="jsonView"
|
||||
isReadOnly
|
||||
mode="javascript"
|
||||
setOptions={EDITOR_SET_OPTIONS}
|
||||
value={value}
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
>
|
||||
{value}
|
||||
</EuiCodeBlock>
|
||||
</EuiCodeEditorContainer>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { EuiContextMenuItem, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
interface AddEndpointExceptionProps {
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const AddEndpointExceptionComponent: React.FC<AddEndpointExceptionProps> = ({
|
||||
onClick,
|
||||
disabled,
|
||||
}) => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key="add-endpoint-exception-menu-item"
|
||||
aria-label={i18n.ACTION_ADD_ENDPOINT_EXCEPTION}
|
||||
data-test-subj="add-endpoint-exception-menu-item"
|
||||
id="addEndpointException"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<EuiText size="m">{i18n.ACTION_ADD_ENDPOINT_EXCEPTION}</EuiText>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddEndpointException = React.memo(AddEndpointExceptionComponent);
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { EuiContextMenuItem, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
interface AddEventFilterProps {
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const AddEventFilterComponent: React.FC<AddEventFilterProps> = ({ onClick, disabled }) => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key="add-event-filter-menu-item"
|
||||
aria-label={i18n.ACTION_ADD_EVENT_FILTER}
|
||||
data-test-subj="add-event-filter-menu-item"
|
||||
id="addEventFilter"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<EuiText data-test-subj="addEventFilterButton" size="m">
|
||||
{i18n.ACTION_ADD_EVENT_FILTER}
|
||||
</EuiText>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddEventFilter = React.memo(AddEventFilterComponent);
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { EuiContextMenuItem, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
interface AddExceptionProps {
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const AddExceptionComponent: React.FC<AddExceptionProps> = ({ disabled, onClick }) => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key="add-exception-menu-item"
|
||||
aria-label={i18n.ACTION_ADD_EXCEPTION}
|
||||
data-test-subj="add-exception-menu-item"
|
||||
id="addException"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<EuiText data-test-subj="addExceptionButton" size="m">
|
||||
{i18n.ACTION_ADD_EXCEPTION}
|
||||
</EuiText>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddException = React.memo(AddExceptionComponent);
|
|
@ -6,53 +6,33 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiPopover,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { EuiButtonIcon, EuiContextMenu, EuiPopover, EuiToolTip } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import { indexOf } from 'lodash';
|
||||
|
||||
import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { get, getOr } from 'lodash/fp';
|
||||
import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
import {
|
||||
DEFAULT_INDEX_PATTERN,
|
||||
DEFAULT_INDEX_PATTERN_EXPERIMENTAL,
|
||||
} from '../../../../../common/constants';
|
||||
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { timelineActions } from '../../../../timelines/store/timeline';
|
||||
import { EventsTdContent } from '../../../../timelines/components/timeline/styles';
|
||||
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../../timelines/components/timeline/helpers';
|
||||
import { FILTER_OPEN, FILTER_CLOSED, FILTER_IN_PROGRESS } from '../alerts_filter_group';
|
||||
import { updateAlertStatusAction } from '../actions';
|
||||
import { SetEventsDeletedProps, SetEventsLoadingProps } from '../types';
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
import {
|
||||
AddExceptionModal,
|
||||
AddExceptionModalProps,
|
||||
} from '../../../../common/components/exceptions/add_exception_modal';
|
||||
import * as i18nCommon from '../../../../common/translations';
|
||||
import * as i18n from '../translations';
|
||||
import {
|
||||
useStateToaster,
|
||||
displaySuccessToast,
|
||||
displayErrorToast,
|
||||
} from '../../../../common/components/toasters';
|
||||
import { inputsModel } from '../../../../common/store';
|
||||
import { useUserData } from '../../user_info';
|
||||
import { AlertData, EcsHit } from '../../../../common/components/exceptions/types';
|
||||
import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query';
|
||||
import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index';
|
||||
import { EventFiltersModal } from '../../../../management/pages/event_filters/view/components/modal';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { useAlertsActions } from './use_alerts_actions';
|
||||
import { useExceptionModal } from './use_add_exception_modal';
|
||||
import { useExceptionActions } from './use_add_exception_actions';
|
||||
import { useEventFilterModal } from './use_event_filter_modal';
|
||||
import { useEventFilterAction } from './use_event_filter_action';
|
||||
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
|
||||
interface AlertContextMenuProps {
|
||||
ariaLabel?: string;
|
||||
|
@ -71,56 +51,14 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
|
|||
onRuleChange,
|
||||
timelineId,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
const eventId = ecsRowData._id;
|
||||
const ruleId = useMemo(
|
||||
(): string | null =>
|
||||
(ecsRowData.signal?.rule && ecsRowData.signal.rule.id && ecsRowData.signal.rule.id[0]) ??
|
||||
null,
|
||||
[ecsRowData]
|
||||
);
|
||||
const ruleName = useMemo(
|
||||
(): string =>
|
||||
(ecsRowData.signal?.rule && ecsRowData.signal.rule.name && ecsRowData.signal.rule.name[0]) ??
|
||||
'',
|
||||
[ecsRowData]
|
||||
);
|
||||
|
||||
// TODO: Steph/ueba remove when past experimental
|
||||
const uebaEnabled = useIsExperimentalFeatureEnabled('uebaEnabled');
|
||||
const ruleId = get(0, ecsRowData?.signal?.rule?.id);
|
||||
const ruleName = get(0, ecsRowData?.signal?.rule?.name);
|
||||
|
||||
const alertStatus = get(0, ecsRowData?.signal?.status) as Status;
|
||||
|
||||
const isEvent = useMemo(() => indexOf(ecsRowData.event?.kind, 'event') !== -1, [ecsRowData]);
|
||||
const ruleIndices = useMemo((): string[] => {
|
||||
if (
|
||||
ecsRowData.signal?.rule &&
|
||||
ecsRowData.signal.rule.index &&
|
||||
ecsRowData.signal.rule.index.length > 0
|
||||
) {
|
||||
return ecsRowData.signal.rule.index;
|
||||
} else {
|
||||
return uebaEnabled
|
||||
? [...DEFAULT_INDEX_PATTERN, ...DEFAULT_INDEX_PATTERN_EXPERIMENTAL]
|
||||
: DEFAULT_INDEX_PATTERN;
|
||||
}
|
||||
}, [ecsRowData.signal?.rule, uebaEnabled]);
|
||||
|
||||
const { addWarning } = useAppToasts();
|
||||
|
||||
const alertStatus = useMemo(() => {
|
||||
return ecsRowData.signal?.status && (ecsRowData.signal.status[0] as Status);
|
||||
}, [ecsRowData]);
|
||||
|
||||
const onButtonClick = useCallback(() => {
|
||||
setPopover(!isPopoverOpen);
|
||||
}, [isPopoverOpen]);
|
||||
|
||||
const closePopover = useCallback((): void => {
|
||||
setPopover(false);
|
||||
}, []);
|
||||
const [exceptionModalType, setOpenAddExceptionModal] = useState<ExceptionListType | null>(null);
|
||||
const [isAddEventFilterModalOpen, setIsAddEventFilterModalOpen] = useState<boolean>(false);
|
||||
const [{ canUserCRUD, hasIndexWrite, hasIndexMaintenance, hasIndexUpdateDelete }] = useUserData();
|
||||
|
||||
const isEndpointAlert = useMemo((): boolean => {
|
||||
if (ecsRowData == null) {
|
||||
|
@ -133,188 +71,14 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
|
|||
return eventModules.includes('endpoint') && kinds.includes('alert');
|
||||
}, [ecsRowData]);
|
||||
|
||||
const closeAddExceptionModal = useCallback((): void => {
|
||||
setOpenAddExceptionModal(null);
|
||||
const onButtonClick = useCallback(() => {
|
||||
setPopover(!isPopoverOpen);
|
||||
}, [isPopoverOpen]);
|
||||
|
||||
const closePopover = useCallback((): void => {
|
||||
setPopover(false);
|
||||
}, []);
|
||||
|
||||
const closeAddEventFilterModal = useCallback((): void => {
|
||||
setIsAddEventFilterModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const onAddExceptionCancel = useCallback(() => {
|
||||
closeAddExceptionModal();
|
||||
}, [closeAddExceptionModal]);
|
||||
|
||||
const onAddExceptionConfirm = useCallback(
|
||||
(didCloseAlert: boolean, didBulkCloseAlert) => {
|
||||
closeAddExceptionModal();
|
||||
if (timelineId !== TimelineId.active || didBulkCloseAlert) {
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
[closeAddExceptionModal, timelineId, refetch]
|
||||
);
|
||||
|
||||
const onAlertStatusUpdateSuccess = useCallback(
|
||||
(updated: number, conflicts: number, newStatus: Status) => {
|
||||
if (conflicts > 0) {
|
||||
// Partial failure
|
||||
addWarning({
|
||||
title: i18nCommon.UPDATE_ALERT_STATUS_FAILED(conflicts),
|
||||
text: i18nCommon.UPDATE_ALERT_STATUS_FAILED_DETAILED(updated, conflicts),
|
||||
});
|
||||
} else {
|
||||
let title: string;
|
||||
switch (newStatus) {
|
||||
case 'closed':
|
||||
title = i18n.CLOSED_ALERT_SUCCESS_TOAST(updated);
|
||||
break;
|
||||
case 'open':
|
||||
title = i18n.OPENED_ALERT_SUCCESS_TOAST(updated);
|
||||
break;
|
||||
case 'in-progress':
|
||||
title = i18n.IN_PROGRESS_ALERT_SUCCESS_TOAST(updated);
|
||||
}
|
||||
displaySuccessToast(title, dispatchToaster);
|
||||
}
|
||||
},
|
||||
[dispatchToaster, addWarning]
|
||||
);
|
||||
|
||||
const onAlertStatusUpdateFailure = useCallback(
|
||||
(newStatus: Status, error: Error) => {
|
||||
let title: string;
|
||||
switch (newStatus) {
|
||||
case 'closed':
|
||||
title = i18n.CLOSED_ALERT_FAILED_TOAST;
|
||||
break;
|
||||
case 'open':
|
||||
title = i18n.OPENED_ALERT_FAILED_TOAST;
|
||||
break;
|
||||
case 'in-progress':
|
||||
title = i18n.IN_PROGRESS_ALERT_FAILED_TOAST;
|
||||
}
|
||||
displayErrorToast(title, [error.message], dispatchToaster);
|
||||
},
|
||||
[dispatchToaster]
|
||||
);
|
||||
|
||||
const setEventsLoading = useCallback(
|
||||
({ eventIds, isLoading }: SetEventsLoadingProps) => {
|
||||
dispatch(timelineActions.setEventsLoading({ id: timelineId, eventIds, isLoading }));
|
||||
},
|
||||
[dispatch, timelineId]
|
||||
);
|
||||
|
||||
const setEventsDeleted = useCallback(
|
||||
({ eventIds, isDeleted }: SetEventsDeletedProps) => {
|
||||
dispatch(timelineActions.setEventsDeleted({ id: timelineId, eventIds, isDeleted }));
|
||||
},
|
||||
[dispatch, timelineId]
|
||||
);
|
||||
|
||||
const openAlertActionOnClick = useCallback(() => {
|
||||
updateAlertStatusAction({
|
||||
alertIds: [eventId],
|
||||
onAlertStatusUpdateFailure,
|
||||
onAlertStatusUpdateSuccess,
|
||||
setEventsDeleted,
|
||||
setEventsLoading,
|
||||
selectedStatus: FILTER_OPEN,
|
||||
});
|
||||
closePopover();
|
||||
}, [
|
||||
closePopover,
|
||||
eventId,
|
||||
onAlertStatusUpdateFailure,
|
||||
onAlertStatusUpdateSuccess,
|
||||
setEventsDeleted,
|
||||
setEventsLoading,
|
||||
]);
|
||||
|
||||
const openAlertActionComponent = useMemo(() => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key="open-alert"
|
||||
aria-label="Open alert"
|
||||
data-test-subj="open-alert-status"
|
||||
id={FILTER_OPEN}
|
||||
onClick={openAlertActionOnClick}
|
||||
disabled={!hasIndexUpdateDelete && !hasIndexMaintenance}
|
||||
>
|
||||
<EuiText size="m">{i18n.ACTION_OPEN_ALERT}</EuiText>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}, [openAlertActionOnClick, hasIndexUpdateDelete, hasIndexMaintenance]);
|
||||
|
||||
const closeAlertActionClick = useCallback(() => {
|
||||
updateAlertStatusAction({
|
||||
alertIds: [eventId],
|
||||
onAlertStatusUpdateFailure,
|
||||
onAlertStatusUpdateSuccess,
|
||||
setEventsDeleted,
|
||||
setEventsLoading,
|
||||
selectedStatus: FILTER_CLOSED,
|
||||
});
|
||||
closePopover();
|
||||
}, [
|
||||
closePopover,
|
||||
eventId,
|
||||
onAlertStatusUpdateFailure,
|
||||
onAlertStatusUpdateSuccess,
|
||||
setEventsDeleted,
|
||||
setEventsLoading,
|
||||
]);
|
||||
|
||||
const closeAlertActionComponent = useMemo(() => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key="close-alert"
|
||||
aria-label="Close alert"
|
||||
data-test-subj="close-alert-status"
|
||||
id={FILTER_CLOSED}
|
||||
onClick={closeAlertActionClick}
|
||||
disabled={!hasIndexUpdateDelete && !hasIndexMaintenance}
|
||||
>
|
||||
<EuiText size="m">{i18n.ACTION_CLOSE_ALERT}</EuiText>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}, [closeAlertActionClick, hasIndexUpdateDelete, hasIndexMaintenance]);
|
||||
|
||||
const inProgressAlertActionClick = useCallback(() => {
|
||||
updateAlertStatusAction({
|
||||
alertIds: [eventId],
|
||||
onAlertStatusUpdateFailure,
|
||||
onAlertStatusUpdateSuccess,
|
||||
setEventsDeleted,
|
||||
setEventsLoading,
|
||||
selectedStatus: FILTER_IN_PROGRESS,
|
||||
});
|
||||
closePopover();
|
||||
}, [
|
||||
closePopover,
|
||||
eventId,
|
||||
onAlertStatusUpdateFailure,
|
||||
onAlertStatusUpdateSuccess,
|
||||
setEventsDeleted,
|
||||
setEventsLoading,
|
||||
]);
|
||||
|
||||
const inProgressAlertActionComponent = useMemo(() => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key="in-progress-alert"
|
||||
aria-label="Mark alert in progress"
|
||||
data-test-subj="in-progress-alert-status"
|
||||
id={FILTER_IN_PROGRESS}
|
||||
onClick={inProgressAlertActionClick}
|
||||
disabled={!canUserCRUD || !hasIndexUpdateDelete}
|
||||
>
|
||||
<EuiText size="m">{i18n.ACTION_IN_PROGRESS_ALERT}</EuiText>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}, [canUserCRUD, hasIndexUpdateDelete, inProgressAlertActionClick]);
|
||||
|
||||
const button = useMemo(() => {
|
||||
return (
|
||||
<EuiToolTip position="top" content={i18n.MORE_ACTIONS}>
|
||||
|
@ -330,105 +94,61 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
|
|||
);
|
||||
}, [disabled, onButtonClick, ariaLabel]);
|
||||
|
||||
const handleAddEndpointExceptionClick = useCallback((): void => {
|
||||
closePopover();
|
||||
setOpenAddExceptionModal('endpoint');
|
||||
}, [closePopover]);
|
||||
const {
|
||||
exceptionModalType,
|
||||
onAddExceptionCancel,
|
||||
onAddExceptionConfirm,
|
||||
onAddExceptionTypeClick,
|
||||
ruleIndices,
|
||||
} = useExceptionModal({
|
||||
ruleIndex: ecsRowData?.signal?.rule?.index,
|
||||
refetch,
|
||||
timelineId,
|
||||
});
|
||||
|
||||
const addEndpointExceptionComponent = useMemo(() => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key="add-endpoint-exception-menu-item"
|
||||
aria-label="Add Endpoint Exception"
|
||||
data-test-subj="add-endpoint-exception-menu-item"
|
||||
id="addEndpointException"
|
||||
onClick={handleAddEndpointExceptionClick}
|
||||
disabled={!canUserCRUD || !hasIndexWrite || !isEndpointAlert}
|
||||
>
|
||||
<EuiText size="m">{i18n.ACTION_ADD_ENDPOINT_EXCEPTION}</EuiText>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}, [canUserCRUD, hasIndexWrite, isEndpointAlert, handleAddEndpointExceptionClick]);
|
||||
const {
|
||||
closeAddEventFilterModal,
|
||||
isAddEventFilterModalOpen,
|
||||
onAddEventFilterClick,
|
||||
} = useEventFilterModal();
|
||||
|
||||
const handleAddExceptionClick = useCallback((): void => {
|
||||
closePopover();
|
||||
setOpenAddExceptionModal('detection');
|
||||
}, [closePopover]);
|
||||
const { statusActions } = useAlertsActions({
|
||||
alertStatus,
|
||||
eventId: ecsRowData?._id,
|
||||
timelineId,
|
||||
closePopover,
|
||||
});
|
||||
|
||||
const addExceptionComponent = useMemo(() => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key="add-exception-menu-item"
|
||||
aria-label="Add Exception"
|
||||
data-test-subj="add-exception-menu-item"
|
||||
id="addException"
|
||||
onClick={handleAddExceptionClick}
|
||||
disabled={!canUserCRUD || !hasIndexWrite}
|
||||
>
|
||||
<EuiText data-test-subj="addExceptionButton" size="m">
|
||||
{i18n.ACTION_ADD_EXCEPTION}
|
||||
</EuiText>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}, [handleAddExceptionClick, canUserCRUD, hasIndexWrite]);
|
||||
|
||||
const handleAddEventFilterClick = useCallback((): void => {
|
||||
closePopover();
|
||||
setIsAddEventFilterModalOpen(true);
|
||||
}, [closePopover]);
|
||||
|
||||
const addEventFilterComponent = useMemo(
|
||||
() => (
|
||||
<EuiContextMenuItem
|
||||
key="add-event-filter-menu-item"
|
||||
aria-label="Add event filter"
|
||||
data-test-subj="add-event-filter-menu-item"
|
||||
id="addEventFilter"
|
||||
onClick={handleAddEventFilterClick}
|
||||
>
|
||||
<EuiText data-test-subj="addEventFilterButton" size="m">
|
||||
{i18n.ACTION_ADD_EVENT_FILTER}
|
||||
</EuiText>
|
||||
</EuiContextMenuItem>
|
||||
),
|
||||
[handleAddEventFilterClick]
|
||||
const handleOnAddExceptionTypeClick = useCallback(
|
||||
(type: ExceptionListType) => {
|
||||
onAddExceptionTypeClick(type);
|
||||
closePopover();
|
||||
},
|
||||
[closePopover, onAddExceptionTypeClick]
|
||||
);
|
||||
|
||||
const statusFilters = useMemo(() => {
|
||||
if (!alertStatus) {
|
||||
return [];
|
||||
}
|
||||
const handleOnAddEventFilterClick = useCallback(() => {
|
||||
onAddEventFilterClick();
|
||||
closePopover();
|
||||
}, [closePopover, onAddEventFilterClick]);
|
||||
|
||||
switch (alertStatus) {
|
||||
case 'open':
|
||||
return [inProgressAlertActionComponent, closeAlertActionComponent];
|
||||
case 'in-progress':
|
||||
return [openAlertActionComponent, closeAlertActionComponent];
|
||||
case 'closed':
|
||||
return [openAlertActionComponent, inProgressAlertActionComponent];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}, [
|
||||
closeAlertActionComponent,
|
||||
inProgressAlertActionComponent,
|
||||
openAlertActionComponent,
|
||||
alertStatus,
|
||||
]);
|
||||
const exceptionActions = useExceptionActions({
|
||||
isEndpointAlert,
|
||||
onAddExceptionTypeClick: handleOnAddExceptionTypeClick,
|
||||
});
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
!isEvent && ruleId
|
||||
? [...statusFilters, addEndpointExceptionComponent, addExceptionComponent]
|
||||
: [addEventFilterComponent],
|
||||
[
|
||||
addEndpointExceptionComponent,
|
||||
addExceptionComponent,
|
||||
addEventFilterComponent,
|
||||
statusFilters,
|
||||
ruleId,
|
||||
isEvent,
|
||||
]
|
||||
const eventFilterActions = useEventFilterAction({
|
||||
onAddEventFilterClick: handleOnAddEventFilterClick,
|
||||
});
|
||||
|
||||
const panels = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 0,
|
||||
items: !isEvent && ruleId ? [...statusActions, ...exceptionActions] : [eventFilterActions],
|
||||
},
|
||||
],
|
||||
[eventFilterActions, exceptionActions, isEvent, ruleId, statusActions]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -444,23 +164,26 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
|
|||
anchorPosition="downLeft"
|
||||
repositionOnScroll
|
||||
>
|
||||
<ContextMenuPanel items={items} />
|
||||
<EuiContextMenu size="s" initialPanelId={0} panels={panels} />
|
||||
</EuiPopover>
|
||||
</EventsTdContent>
|
||||
</div>
|
||||
{exceptionModalType != null && ruleId != null && ecsRowData != null && (
|
||||
<AddExceptionModalWrapper
|
||||
ruleName={ruleName}
|
||||
ruleId={ruleId}
|
||||
ruleIndices={ruleIndices}
|
||||
exceptionListType={exceptionModalType}
|
||||
ecsData={ecsRowData}
|
||||
onCancel={onAddExceptionCancel}
|
||||
onConfirm={onAddExceptionConfirm}
|
||||
alertStatus={alertStatus}
|
||||
onRuleChange={onRuleChange}
|
||||
/>
|
||||
)}
|
||||
{exceptionModalType != null &&
|
||||
ruleId != null &&
|
||||
ruleName != null &&
|
||||
ecsRowData?._id != null && (
|
||||
<AddExceptionModalWrapper
|
||||
ruleName={ruleName}
|
||||
ruleId={ruleId}
|
||||
ruleIndices={ruleIndices}
|
||||
exceptionListType={exceptionModalType}
|
||||
eventId={ecsRowData?._id}
|
||||
onCancel={onAddExceptionCancel}
|
||||
onConfirm={onAddExceptionConfirm}
|
||||
alertStatus={alertStatus}
|
||||
onRuleChange={onRuleChange}
|
||||
/>
|
||||
)}
|
||||
{isAddEventFilterModalOpen && ecsRowData != null && (
|
||||
<EventFiltersModal data={ecsRowData} onCancel={closeAddEventFilterModal} />
|
||||
)}
|
||||
|
@ -468,7 +191,7 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const ContextMenuPanel = styled(EuiContextMenuPanel)`
|
||||
const ContextMenuPanel = styled(EuiContextMenu)`
|
||||
font-size: ${({ theme }) => theme.eui.euiFontSizeS};
|
||||
`;
|
||||
|
||||
|
@ -480,7 +203,7 @@ type AddExceptionModalWrapperProps = Omit<
|
|||
AddExceptionModalProps,
|
||||
'alertData' | 'isAlertDataLoading'
|
||||
> & {
|
||||
ecsData: Ecs;
|
||||
eventId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -488,12 +211,12 @@ type AddExceptionModalWrapperProps = Omit<
|
|||
* Due to the conditional nature of the modal and how we use the `ecsData` field,
|
||||
* we cannot use the fetch hook within the modal component itself
|
||||
*/
|
||||
const AddExceptionModalWrapper: React.FC<AddExceptionModalWrapperProps> = ({
|
||||
export const AddExceptionModalWrapper: React.FC<AddExceptionModalWrapperProps> = ({
|
||||
ruleName,
|
||||
ruleId,
|
||||
ruleIndices,
|
||||
exceptionListType,
|
||||
ecsData,
|
||||
eventId,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
alertStatus,
|
||||
|
@ -502,7 +225,7 @@ const AddExceptionModalWrapper: React.FC<AddExceptionModalWrapperProps> = ({
|
|||
const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex();
|
||||
|
||||
const { loading: isLoadingAlertData, data } = useQueryAlerts<EcsHit, {}>({
|
||||
query: buildGetAlertByIdQuery(ecsData?._id),
|
||||
query: buildGetAlertByIdQuery(eventId),
|
||||
indexName: signalIndexName,
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { EuiContextMenuItem, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FILTER_CLOSED } from '../../alerts_filter_group';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
interface CloseAlertActionProps {
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const CloseAlertActionComponent: React.FC<CloseAlertActionProps> = ({ onClick, disabled }) => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key="close-alert"
|
||||
aria-label={i18n.ACTION_CLOSE_ALERT}
|
||||
data-test-subj="close-alert-status"
|
||||
id={FILTER_CLOSED}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<EuiText size="m">{i18n.ACTION_CLOSE_ALERT}</EuiText>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const CloseAlertAction = React.memo(CloseAlertActionComponent);
|
|
@ -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 { EuiContextMenuItem, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FILTER_IN_PROGRESS } from '../../alerts_filter_group';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
interface InProgressAlertStatusProps {
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const InProgressAlertStatusComponent: React.FC<InProgressAlertStatusProps> = ({
|
||||
onClick,
|
||||
disabled,
|
||||
}) => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key="in-progress-alert"
|
||||
aria-label={i18n.ACTION_IN_PROGRESS_ALERT}
|
||||
data-test-subj="in-progress-alert-status"
|
||||
id={FILTER_IN_PROGRESS}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<EuiText size="m">{i18n.ACTION_IN_PROGRESS_ALERT}</EuiText>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const InProgressAlertStatus = React.memo(InProgressAlertStatusComponent);
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { EuiContextMenuItem, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FILTER_OPEN } from '../../alerts_filter_group';
|
||||
import * as i18n from '../../translations';
|
||||
|
||||
interface OpenAlertStatusProps {
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const OpenAlertStatusComponent: React.FC<OpenAlertStatusProps> = ({ onClick, disabled }) => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key="open-alert"
|
||||
aria-label={i18n.ACTION_OPEN_ALERT}
|
||||
data-test-subj="open-alert-status"
|
||||
id={FILTER_OPEN}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<EuiText size="m">{i18n.ACTION_OPEN_ALERT}</EuiText>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const OpenAlertStatus = React.memo(OpenAlertStatusComponent);
|
|
@ -5,102 +5,41 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import React from 'react';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline';
|
||||
import { timelineActions } from '../../../../timelines/store/timeline';
|
||||
import { sendAlertToTimelineAction } from '../actions';
|
||||
import { dispatchUpdateTimeline } from '../../../../timelines/components/open_timeline/helpers';
|
||||
import { ActionIconItem } from '../../../../timelines/components/timeline/body/actions/action_icon_item';
|
||||
import { CreateTimelineProps } from '../types';
|
||||
|
||||
import {
|
||||
ACTION_INVESTIGATE_IN_TIMELINE,
|
||||
ACTION_INVESTIGATE_IN_TIMELINE_ARIA_LABEL,
|
||||
} from '../translations';
|
||||
import { useInvestigateInTimeline } from './use_investigate_in_timeline';
|
||||
|
||||
interface InvestigateInTimelineActionProps {
|
||||
ecsRowData: Ecs | Ecs[] | null;
|
||||
nonEcsRowData: TimelineNonEcsData[];
|
||||
ecsRowData?: Ecs | Ecs[] | null;
|
||||
nonEcsRowData?: TimelineNonEcsData[];
|
||||
ariaLabel?: string;
|
||||
alertIds?: string[];
|
||||
fetchEcsAlertsData?: (alertIds?: string[]) => Promise<Ecs[]>;
|
||||
buttonType?: 'text' | 'icon';
|
||||
onInvestigateInTimelineAlertClick?: () => void;
|
||||
}
|
||||
|
||||
const InvestigateInTimelineActionComponent: React.FC<InvestigateInTimelineActionProps> = ({
|
||||
ariaLabel = ACTION_INVESTIGATE_IN_TIMELINE_ARIA_LABEL,
|
||||
alertIds,
|
||||
ecsRowData,
|
||||
fetchEcsAlertsData,
|
||||
nonEcsRowData,
|
||||
buttonType,
|
||||
onInvestigateInTimelineAlertClick,
|
||||
}) => {
|
||||
const {
|
||||
data: { search: searchStrategyClient },
|
||||
} = useKibana().services;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const updateTimelineIsLoading = useCallback(
|
||||
(payload) => dispatch(timelineActions.updateIsLoading(payload)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const createTimeline = useCallback(
|
||||
({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => {
|
||||
updateTimelineIsLoading({ id: TimelineId.active, isLoading: false });
|
||||
dispatchUpdateTimeline(dispatch)({
|
||||
duplicate: true,
|
||||
from: fromTimeline,
|
||||
id: TimelineId.active,
|
||||
notes: [],
|
||||
timeline: {
|
||||
...timeline,
|
||||
// by setting as an empty array, it will default to all in the reducer because of the event type
|
||||
indexNames: [],
|
||||
show: true,
|
||||
},
|
||||
to: toTimeline,
|
||||
ruleNote,
|
||||
})();
|
||||
},
|
||||
[dispatch, updateTimelineIsLoading]
|
||||
);
|
||||
|
||||
const investigateInTimelineAlertClick = useCallback(async () => {
|
||||
try {
|
||||
if (ecsRowData != null) {
|
||||
await sendAlertToTimelineAction({
|
||||
createTimeline,
|
||||
ecsData: ecsRowData,
|
||||
nonEcsData: nonEcsRowData,
|
||||
searchStrategyClient,
|
||||
updateTimelineIsLoading,
|
||||
});
|
||||
}
|
||||
if (ecsRowData == null && fetchEcsAlertsData) {
|
||||
const alertsEcsData = await fetchEcsAlertsData(alertIds);
|
||||
await sendAlertToTimelineAction({
|
||||
createTimeline,
|
||||
ecsData: alertsEcsData,
|
||||
nonEcsData: nonEcsRowData,
|
||||
searchStrategyClient,
|
||||
updateTimelineIsLoading,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// TODO show a toaster that something went wrong
|
||||
}
|
||||
}, [
|
||||
alertIds,
|
||||
createTimeline,
|
||||
const { investigateInTimelineAlertClick } = useInvestigateInTimeline({
|
||||
ecsRowData,
|
||||
fetchEcsAlertsData,
|
||||
nonEcsRowData,
|
||||
searchStrategyClient,
|
||||
updateTimelineIsLoading,
|
||||
]);
|
||||
alertIds,
|
||||
onInvestigateInTimelineAlertClick,
|
||||
});
|
||||
|
||||
return (
|
||||
<ActionIconItem
|
||||
|
@ -110,6 +49,7 @@ const InvestigateInTimelineActionComponent: React.FC<InvestigateInTimelineAction
|
|||
iconType="timeline"
|
||||
onClick={investigateInTimelineAlertClick}
|
||||
isDisabled={false}
|
||||
buttonType={buttonType}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { useCallback, useMemo } from 'react';
|
||||
import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { useUserData } from '../../user_info';
|
||||
import { ACTION_ADD_ENDPOINT_EXCEPTION, ACTION_ADD_EXCEPTION } from '../translations';
|
||||
|
||||
interface UseExceptionActions {
|
||||
name: string;
|
||||
onClick: () => void;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
interface UseExceptionActionProps {
|
||||
isEndpointAlert: boolean;
|
||||
onAddExceptionTypeClick: (type: ExceptionListType) => void;
|
||||
}
|
||||
|
||||
export const useExceptionActions = ({
|
||||
isEndpointAlert,
|
||||
onAddExceptionTypeClick,
|
||||
}: UseExceptionActionProps): UseExceptionActions[] => {
|
||||
const [{ canUserCRUD, hasIndexWrite }] = useUserData();
|
||||
|
||||
const handleDetectionExceptionModal = useCallback(() => {
|
||||
onAddExceptionTypeClick('detection');
|
||||
}, [onAddExceptionTypeClick]);
|
||||
|
||||
const handleEndpointExceptionModal = useCallback(() => {
|
||||
onAddExceptionTypeClick('endpoint');
|
||||
}, [onAddExceptionTypeClick]);
|
||||
|
||||
const disabledAddEndpointException = !canUserCRUD || !hasIndexWrite || !isEndpointAlert;
|
||||
const disabledAddException = !canUserCRUD || !hasIndexWrite;
|
||||
|
||||
const exceptionActions = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: ACTION_ADD_ENDPOINT_EXCEPTION,
|
||||
onClick: handleEndpointExceptionModal,
|
||||
disabled: disabledAddEndpointException,
|
||||
[`data-test-subj`]: 'add-endpoint-exception-menu-item',
|
||||
},
|
||||
{
|
||||
name: ACTION_ADD_EXCEPTION,
|
||||
onClick: handleDetectionExceptionModal,
|
||||
disabled: disabledAddException,
|
||||
[`data-test-subj`]: 'add-exception-menu-item',
|
||||
},
|
||||
],
|
||||
[
|
||||
disabledAddEndpointException,
|
||||
disabledAddException,
|
||||
handleDetectionExceptionModal,
|
||||
handleEndpointExceptionModal,
|
||||
]
|
||||
);
|
||||
|
||||
return exceptionActions;
|
||||
};
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 { useCallback, useMemo, useState } from 'react';
|
||||
import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import {
|
||||
DEFAULT_INDEX_PATTERN,
|
||||
DEFAULT_INDEX_PATTERN_EXPERIMENTAL,
|
||||
} from '../../../../../common/constants';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
import { inputsModel } from '../../../../common/store';
|
||||
|
||||
interface UseExceptionModalProps {
|
||||
ruleIndex: string[] | null | undefined;
|
||||
refetch?: inputsModel.Refetch;
|
||||
timelineId: string;
|
||||
}
|
||||
interface UseExceptionModal {
|
||||
exceptionModalType: ExceptionListType | null;
|
||||
onAddExceptionTypeClick: (type: ExceptionListType) => void;
|
||||
onAddExceptionCancel: () => void;
|
||||
onAddExceptionConfirm: (didCloseAlert: boolean, didBulkCloseAlert: boolean) => void;
|
||||
ruleIndices: string[];
|
||||
}
|
||||
|
||||
export const useExceptionModal = ({
|
||||
ruleIndex,
|
||||
refetch,
|
||||
timelineId,
|
||||
}: UseExceptionModalProps): UseExceptionModal => {
|
||||
const [exceptionModalType, setOpenAddExceptionModal] = useState<ExceptionListType | null>(null);
|
||||
|
||||
// TODO: Steph/ueba remove when past experimental
|
||||
const uebaEnabled = useIsExperimentalFeatureEnabled('uebaEnabled');
|
||||
|
||||
const ruleIndices = useMemo((): string[] => {
|
||||
if (ruleIndex != null) {
|
||||
return ruleIndex;
|
||||
} else {
|
||||
return uebaEnabled
|
||||
? [...DEFAULT_INDEX_PATTERN, ...DEFAULT_INDEX_PATTERN_EXPERIMENTAL]
|
||||
: DEFAULT_INDEX_PATTERN;
|
||||
}
|
||||
}, [ruleIndex, uebaEnabled]);
|
||||
|
||||
const onAddExceptionTypeClick = useCallback((exceptionListType: ExceptionListType): void => {
|
||||
setOpenAddExceptionModal(exceptionListType);
|
||||
}, []);
|
||||
|
||||
const onAddExceptionCancel = useCallback(() => {
|
||||
setOpenAddExceptionModal(null);
|
||||
}, []);
|
||||
|
||||
const onAddExceptionConfirm = useCallback(
|
||||
(didCloseAlert: boolean, didBulkCloseAlert) => {
|
||||
if (refetch && (timelineId !== TimelineId.active || didBulkCloseAlert)) {
|
||||
refetch();
|
||||
}
|
||||
setOpenAddExceptionModal(null);
|
||||
},
|
||||
[refetch, timelineId]
|
||||
);
|
||||
|
||||
return {
|
||||
exceptionModalType,
|
||||
onAddExceptionTypeClick,
|
||||
onAddExceptionCancel,
|
||||
onAddExceptionConfirm,
|
||||
ruleIndices,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* 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 { useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { timelineActions } from '../../../../timelines/store/timeline';
|
||||
import { FILTER_OPEN, FILTER_CLOSED, FILTER_IN_PROGRESS } from '../alerts_filter_group';
|
||||
import { updateAlertStatusAction } from '../actions';
|
||||
import { SetEventsDeletedProps, SetEventsLoadingProps } from '../types';
|
||||
import * as i18nCommon from '../../../../common/translations';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
import {
|
||||
useStateToaster,
|
||||
displaySuccessToast,
|
||||
displayErrorToast,
|
||||
} from '../../../../common/components/toasters';
|
||||
import { useUserData } from '../../user_info';
|
||||
|
||||
interface Props {
|
||||
alertStatus?: string;
|
||||
closePopover: () => void;
|
||||
eventId: string | null | undefined;
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
export const useAlertsActions = ({ alertStatus, closePopover, eventId, timelineId }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
const { addWarning } = useAppToasts();
|
||||
|
||||
const [{ canUserCRUD, hasIndexMaintenance, hasIndexUpdateDelete }] = useUserData();
|
||||
|
||||
const onAlertStatusUpdateSuccess = useCallback(
|
||||
(updated: number, conflicts: number, newStatus: Status) => {
|
||||
if (conflicts > 0) {
|
||||
// Partial failure
|
||||
addWarning({
|
||||
title: i18nCommon.UPDATE_ALERT_STATUS_FAILED(conflicts),
|
||||
text: i18nCommon.UPDATE_ALERT_STATUS_FAILED_DETAILED(updated, conflicts),
|
||||
});
|
||||
} else {
|
||||
let title: string;
|
||||
switch (newStatus) {
|
||||
case 'closed':
|
||||
title = i18n.CLOSED_ALERT_SUCCESS_TOAST(updated);
|
||||
break;
|
||||
case 'open':
|
||||
title = i18n.OPENED_ALERT_SUCCESS_TOAST(updated);
|
||||
break;
|
||||
case 'in-progress':
|
||||
title = i18n.IN_PROGRESS_ALERT_SUCCESS_TOAST(updated);
|
||||
}
|
||||
|
||||
displaySuccessToast(title, dispatchToaster);
|
||||
}
|
||||
},
|
||||
[addWarning, dispatchToaster]
|
||||
);
|
||||
|
||||
const onAlertStatusUpdateFailure = useCallback(
|
||||
(newStatus: Status, error: Error) => {
|
||||
let title: string;
|
||||
switch (newStatus) {
|
||||
case 'closed':
|
||||
title = i18n.CLOSED_ALERT_FAILED_TOAST;
|
||||
break;
|
||||
case 'open':
|
||||
title = i18n.OPENED_ALERT_FAILED_TOAST;
|
||||
break;
|
||||
case 'in-progress':
|
||||
title = i18n.IN_PROGRESS_ALERT_FAILED_TOAST;
|
||||
}
|
||||
displayErrorToast(title, [error.message], dispatchToaster);
|
||||
},
|
||||
[dispatchToaster]
|
||||
);
|
||||
|
||||
const setEventsLoading = useCallback(
|
||||
({ eventIds, isLoading }: SetEventsLoadingProps) => {
|
||||
dispatch(timelineActions.setEventsLoading({ id: timelineId, eventIds, isLoading }));
|
||||
},
|
||||
[dispatch, timelineId]
|
||||
);
|
||||
|
||||
const setEventsDeleted = useCallback(
|
||||
({ eventIds, isDeleted }: SetEventsDeletedProps) => {
|
||||
dispatch(timelineActions.setEventsDeleted({ id: timelineId, eventIds, isDeleted }));
|
||||
},
|
||||
[dispatch, timelineId]
|
||||
);
|
||||
|
||||
const openAlertActionOnClick = useCallback(() => {
|
||||
if (eventId) {
|
||||
updateAlertStatusAction({
|
||||
alertIds: [eventId],
|
||||
onAlertStatusUpdateFailure,
|
||||
onAlertStatusUpdateSuccess,
|
||||
setEventsDeleted,
|
||||
setEventsLoading,
|
||||
selectedStatus: FILTER_OPEN,
|
||||
});
|
||||
}
|
||||
closePopover();
|
||||
}, [
|
||||
closePopover,
|
||||
eventId,
|
||||
onAlertStatusUpdateFailure,
|
||||
onAlertStatusUpdateSuccess,
|
||||
setEventsDeleted,
|
||||
setEventsLoading,
|
||||
]);
|
||||
|
||||
const closeAlertActionClick = useCallback(() => {
|
||||
if (eventId) {
|
||||
updateAlertStatusAction({
|
||||
alertIds: [eventId],
|
||||
onAlertStatusUpdateFailure,
|
||||
onAlertStatusUpdateSuccess,
|
||||
setEventsDeleted,
|
||||
setEventsLoading,
|
||||
selectedStatus: FILTER_CLOSED,
|
||||
});
|
||||
}
|
||||
|
||||
closePopover();
|
||||
}, [
|
||||
closePopover,
|
||||
eventId,
|
||||
onAlertStatusUpdateFailure,
|
||||
onAlertStatusUpdateSuccess,
|
||||
setEventsDeleted,
|
||||
setEventsLoading,
|
||||
]);
|
||||
|
||||
const inProgressAlertActionClick = useCallback(() => {
|
||||
if (eventId) {
|
||||
updateAlertStatusAction({
|
||||
alertIds: [eventId],
|
||||
onAlertStatusUpdateFailure,
|
||||
onAlertStatusUpdateSuccess,
|
||||
setEventsDeleted,
|
||||
setEventsLoading,
|
||||
selectedStatus: FILTER_IN_PROGRESS,
|
||||
});
|
||||
}
|
||||
|
||||
closePopover();
|
||||
}, [
|
||||
closePopover,
|
||||
eventId,
|
||||
onAlertStatusUpdateFailure,
|
||||
onAlertStatusUpdateSuccess,
|
||||
setEventsDeleted,
|
||||
setEventsLoading,
|
||||
]);
|
||||
|
||||
const disabledInProgressAlertAction = !canUserCRUD || !hasIndexUpdateDelete;
|
||||
|
||||
const inProgressAlertAction = useMemo(() => {
|
||||
return {
|
||||
name: i18n.ACTION_IN_PROGRESS_ALERT,
|
||||
disabled: disabledInProgressAlertAction,
|
||||
onClick: inProgressAlertActionClick,
|
||||
[`data-test-subj`]: 'in-progress-alert-status',
|
||||
};
|
||||
}, [disabledInProgressAlertAction, inProgressAlertActionClick]);
|
||||
|
||||
const disabledCloseAlertAction = !hasIndexUpdateDelete && !hasIndexMaintenance;
|
||||
const closeAlertAction = useMemo(() => {
|
||||
return {
|
||||
name: i18n.ACTION_CLOSE_ALERT,
|
||||
disabled: disabledCloseAlertAction,
|
||||
onClick: closeAlertActionClick,
|
||||
[`data-test-subj`]: 'close-alert-status',
|
||||
};
|
||||
}, [disabledCloseAlertAction, closeAlertActionClick]);
|
||||
|
||||
const disabledOpenAlertAction = !hasIndexUpdateDelete && !hasIndexMaintenance;
|
||||
const openAlertAction = useMemo(() => {
|
||||
return {
|
||||
name: i18n.ACTION_OPEN_ALERT,
|
||||
disabled: disabledOpenAlertAction,
|
||||
onClick: openAlertActionOnClick,
|
||||
[`data-test-subj`]: 'open-alert-status',
|
||||
};
|
||||
}, [disabledOpenAlertAction, openAlertActionOnClick]);
|
||||
|
||||
const statusActions = useMemo(() => {
|
||||
if (!alertStatus) {
|
||||
return [];
|
||||
}
|
||||
|
||||
switch (alertStatus) {
|
||||
case 'open':
|
||||
return [inProgressAlertAction, closeAlertAction];
|
||||
case 'in-progress':
|
||||
return [openAlertAction, closeAlertAction];
|
||||
case 'closed':
|
||||
return [openAlertAction, inProgressAlertAction];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}, [alertStatus, inProgressAlertAction, closeAlertAction, openAlertAction]);
|
||||
|
||||
return {
|
||||
statusActions,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { ACTION_ADD_EVENT_FILTER } from '../translations';
|
||||
|
||||
export const useEventFilterAction = ({
|
||||
onAddEventFilterClick,
|
||||
}: {
|
||||
onAddEventFilterClick: () => void;
|
||||
}) => {
|
||||
const eventFilterActions = useMemo(
|
||||
() => ({
|
||||
name: ACTION_ADD_EVENT_FILTER,
|
||||
onClick: onAddEventFilterClick,
|
||||
}),
|
||||
[onAddEventFilterClick]
|
||||
);
|
||||
return eventFilterActions;
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { useCallback, useState } from 'react';
|
||||
|
||||
export const useEventFilterModal = () => {
|
||||
const [isAddEventFilterModalOpen, setIsAddEventFilterModalOpen] = useState<boolean>(false);
|
||||
|
||||
const onAddEventFilterClick = useCallback((): void => {
|
||||
setIsAddEventFilterModalOpen(true);
|
||||
}, []);
|
||||
const closeAddEventFilterModal = useCallback((): void => {
|
||||
setIsAddEventFilterModalOpen(false);
|
||||
}, []);
|
||||
|
||||
return { closeAddEventFilterModal, isAddEventFilterModalOpen, onAddEventFilterClick };
|
||||
};
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline';
|
||||
import { timelineActions } from '../../../../timelines/store/timeline';
|
||||
import { sendAlertToTimelineAction } from '../actions';
|
||||
import { dispatchUpdateTimeline } from '../../../../timelines/components/open_timeline/helpers';
|
||||
import { CreateTimelineProps } from '../types';
|
||||
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../translations';
|
||||
import { useFetchEcsAlertsData } from '../../../containers/detection_engine/alerts/use_fetch_ecs_alerts_data';
|
||||
|
||||
interface UseInvestigateInTimelineActionProps {
|
||||
ecsRowData?: Ecs | Ecs[] | null;
|
||||
nonEcsRowData?: TimelineNonEcsData[];
|
||||
alertIds?: string[] | null | undefined;
|
||||
onInvestigateInTimelineAlertClick?: () => void;
|
||||
}
|
||||
|
||||
export const useInvestigateInTimeline = ({
|
||||
ecsRowData,
|
||||
nonEcsRowData,
|
||||
alertIds,
|
||||
onInvestigateInTimelineAlertClick,
|
||||
}: UseInvestigateInTimelineActionProps) => {
|
||||
const {
|
||||
data: { search: searchStrategyClient },
|
||||
} = useKibana().services;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const updateTimelineIsLoading = useCallback(
|
||||
(payload) => dispatch(timelineActions.updateIsLoading(payload)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const createTimeline = useCallback(
|
||||
({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => {
|
||||
updateTimelineIsLoading({ id: TimelineId.active, isLoading: false });
|
||||
dispatchUpdateTimeline(dispatch)({
|
||||
duplicate: true,
|
||||
from: fromTimeline,
|
||||
id: TimelineId.active,
|
||||
notes: [],
|
||||
timeline: {
|
||||
...timeline,
|
||||
// by setting as an empty array, it will default to all in the reducer because of the event type
|
||||
indexNames: [],
|
||||
show: true,
|
||||
},
|
||||
to: toTimeline,
|
||||
ruleNote,
|
||||
})();
|
||||
},
|
||||
[dispatch, updateTimelineIsLoading]
|
||||
);
|
||||
|
||||
const showInvestigateInTimelineAction = alertIds != null;
|
||||
const { isLoading: isFetchingAlertEcs, alertsEcsData } = useFetchEcsAlertsData({
|
||||
alertIds,
|
||||
skip: ecsRowData != null || alertIds == null,
|
||||
});
|
||||
|
||||
const investigateInTimelineAlertClick = useCallback(async () => {
|
||||
if (onInvestigateInTimelineAlertClick) {
|
||||
onInvestigateInTimelineAlertClick();
|
||||
}
|
||||
if (alertsEcsData != null) {
|
||||
await sendAlertToTimelineAction({
|
||||
createTimeline,
|
||||
ecsData: alertsEcsData,
|
||||
nonEcsData: nonEcsRowData ?? [],
|
||||
searchStrategyClient,
|
||||
updateTimelineIsLoading,
|
||||
});
|
||||
}
|
||||
|
||||
if (ecsRowData != null) {
|
||||
await sendAlertToTimelineAction({
|
||||
createTimeline,
|
||||
ecsData: ecsRowData,
|
||||
nonEcsData: nonEcsRowData ?? [],
|
||||
searchStrategyClient,
|
||||
updateTimelineIsLoading,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
alertsEcsData,
|
||||
createTimeline,
|
||||
ecsRowData,
|
||||
nonEcsRowData,
|
||||
onInvestigateInTimelineAlertClick,
|
||||
searchStrategyClient,
|
||||
updateTimelineIsLoading,
|
||||
]);
|
||||
|
||||
const investigateInTimelineAction = showInvestigateInTimelineAction
|
||||
? [
|
||||
{
|
||||
name: ACTION_INVESTIGATE_IN_TIMELINE,
|
||||
onClick: investigateInTimelineAlertClick,
|
||||
disabled: isFetchingAlertEcs,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
return {
|
||||
investigateInTimelineAction,
|
||||
investigateInTimelineAlertClick,
|
||||
showInvestigateInTimelineAction,
|
||||
};
|
||||
};
|
|
@ -178,6 +178,13 @@ export const ACTION_ADD_EVENT_FILTER = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const ACTION_ADD_TO_CASE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.alerts.actions.addToCase',
|
||||
{
|
||||
defaultMessage: 'Add to case',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_ADD_ENDPOINT_EXCEPTION = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.alerts.actions.addEndpointException',
|
||||
{
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { find } from 'lodash/fp';
|
||||
|
||||
import type { TimelineEventsDetailsItem } from '../../../../common';
|
||||
|
||||
export const getFieldValue = (
|
||||
{
|
||||
category,
|
||||
field,
|
||||
}: {
|
||||
category: string;
|
||||
field: string;
|
||||
},
|
||||
data: TimelineEventsDetailsItem[] | null
|
||||
) => {
|
||||
const currentField = find({ category, field }, data)?.values;
|
||||
return currentField && currentField.length > 0 ? currentField[0] : '';
|
||||
};
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* 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, { useState, useCallback, useMemo } from 'react';
|
||||
import { EuiContextMenuItem, EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui';
|
||||
import { ISOLATE_HOST, UNISOLATE_HOST } from './translations';
|
||||
import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations';
|
||||
import { useHostIsolationStatus } from '../../containers/detection_engine/alerts/use_host_isolation_status';
|
||||
import { HostStatus } from '../../../../common/endpoint/types';
|
||||
|
||||
export const TakeActionDropdown = React.memo(
|
||||
({
|
||||
onChange,
|
||||
agentId,
|
||||
}: {
|
||||
onChange: (action: 'isolateHost' | 'unisolateHost') => void;
|
||||
agentId: string;
|
||||
}) => {
|
||||
const { loading, isIsolated: isolationStatus, agentStatus } = useHostIsolationStatus({
|
||||
agentId,
|
||||
});
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const closePopoverHandler = useCallback(() => {
|
||||
setIsPopoverOpen(false);
|
||||
}, []);
|
||||
|
||||
const isolateHostHandler = useCallback(() => {
|
||||
setIsPopoverOpen(false);
|
||||
if (isolationStatus === false) {
|
||||
onChange('isolateHost');
|
||||
} else {
|
||||
onChange('unisolateHost');
|
||||
}
|
||||
}, [onChange, isolationStatus]);
|
||||
|
||||
const takeActionButton = useMemo(() => {
|
||||
return (
|
||||
<EuiButton
|
||||
iconSide="right"
|
||||
fill
|
||||
iconType="arrowDown"
|
||||
disabled={loading || agentStatus === HostStatus.UNENROLLED}
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
}}
|
||||
>
|
||||
{TAKE_ACTION}
|
||||
</EuiButton>
|
||||
);
|
||||
}, [isPopoverOpen, loading, agentStatus]);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="hostIsolationTakeActionPanel"
|
||||
button={takeActionButton}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopoverHandler}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenuPanel size="s">
|
||||
{isolationStatus === false ? (
|
||||
<EuiContextMenuItem key="isolateHost" onClick={isolateHostHandler}>
|
||||
{ISOLATE_HOST}
|
||||
</EuiContextMenuItem>
|
||||
) : (
|
||||
<EuiContextMenuItem key="unisolateHost" onClick={isolateHostHandler}>
|
||||
{UNISOLATE_HOST}
|
||||
</EuiContextMenuItem>
|
||||
)}
|
||||
</EuiContextMenuPanel>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TakeActionDropdown.displayName = 'TakeActionDropdown';
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 { useCallback, useMemo } from 'react';
|
||||
import type { TimelineEventsDetailsItem } from '../../../../common';
|
||||
import { isIsolationSupported } from '../../../../common/endpoint/service/host_isolation/utils';
|
||||
import { HostStatus } from '../../../../common/endpoint/types';
|
||||
import { useIsolationPrivileges } from '../../../common/hooks/endpoint/use_isolate_privileges';
|
||||
import { endpointAlertCheck } from '../../../common/utils/endpoint_alert_check';
|
||||
import { useHostIsolationStatus } from '../../containers/detection_engine/alerts/use_host_isolation_status';
|
||||
import { ISOLATE_HOST, UNISOLATE_HOST } from './translations';
|
||||
import { getFieldValue } from './helpers';
|
||||
|
||||
interface UseHostIsolationActionProps {
|
||||
closePopover: () => void;
|
||||
detailsData: TimelineEventsDetailsItem[] | null;
|
||||
isHostIsolationPanelOpen: boolean;
|
||||
onAddIsolationStatusClick: (action: 'isolateHost' | 'unisolateHost') => void;
|
||||
}
|
||||
|
||||
export const useHostIsolationAction = ({
|
||||
closePopover,
|
||||
detailsData,
|
||||
isHostIsolationPanelOpen,
|
||||
onAddIsolationStatusClick,
|
||||
}: UseHostIsolationActionProps) => {
|
||||
const isEndpointAlert = useMemo(() => {
|
||||
return endpointAlertCheck({ data: detailsData || [] });
|
||||
}, [detailsData]);
|
||||
|
||||
const agentId = useMemo(
|
||||
() => getFieldValue({ category: 'agent', field: 'agent.id' }, detailsData),
|
||||
[detailsData]
|
||||
);
|
||||
|
||||
const hostOsFamily = useMemo(
|
||||
() => getFieldValue({ category: 'host', field: 'host.os.name' }, detailsData),
|
||||
[detailsData]
|
||||
);
|
||||
|
||||
const agentVersion = useMemo(
|
||||
() => getFieldValue({ category: 'agent', field: 'agent.version' }, detailsData),
|
||||
[detailsData]
|
||||
);
|
||||
|
||||
const isolationSupported = isIsolationSupported({
|
||||
osName: hostOsFamily,
|
||||
version: agentVersion,
|
||||
});
|
||||
|
||||
const {
|
||||
loading: loadingHostIsolationStatus,
|
||||
isIsolated: isolationStatus,
|
||||
agentStatus,
|
||||
} = useHostIsolationStatus({
|
||||
agentId,
|
||||
});
|
||||
|
||||
const { isAllowed: isIsolationAllowed } = useIsolationPrivileges();
|
||||
|
||||
const isolateHostHandler = useCallback(() => {
|
||||
closePopover();
|
||||
if (isolationStatus === false) {
|
||||
onAddIsolationStatusClick('isolateHost');
|
||||
} else {
|
||||
onAddIsolationStatusClick('unisolateHost');
|
||||
}
|
||||
}, [closePopover, isolationStatus, onAddIsolationStatusClick]);
|
||||
|
||||
const isolateHostTitle = isolationStatus === false ? ISOLATE_HOST : UNISOLATE_HOST;
|
||||
|
||||
const hostIsolationAction = useMemo(
|
||||
() =>
|
||||
isIsolationAllowed &&
|
||||
isEndpointAlert &&
|
||||
isolationSupported &&
|
||||
isHostIsolationPanelOpen === false
|
||||
? [
|
||||
{
|
||||
name: isolateHostTitle,
|
||||
onClick: isolateHostHandler,
|
||||
disabled: loadingHostIsolationStatus || agentStatus === HostStatus.UNENROLLED,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
[
|
||||
agentStatus,
|
||||
isEndpointAlert,
|
||||
isHostIsolationPanelOpen,
|
||||
isIsolationAllowed,
|
||||
isolateHostHandler,
|
||||
isolateHostTitle,
|
||||
isolationSupported,
|
||||
loadingHostIsolationStatus,
|
||||
]
|
||||
);
|
||||
return hostIsolationAction;
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { ACTION_ADD_TO_CASE } from '../alerts_table/translations';
|
||||
|
||||
export const addToCaseActionItem = (timelineId: string | null | undefined) =>
|
||||
['detections-page', 'detections-rules-details-page', 'timeline-1'].includes(timelineId ?? '')
|
||||
? [
|
||||
{
|
||||
name: ACTION_ADD_TO_CASE,
|
||||
panel: 2,
|
||||
},
|
||||
]
|
||||
: [];
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* 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, { useState, useCallback, useMemo } from 'react';
|
||||
import { EuiContextMenu, EuiButton, EuiPopover } from '@elastic/eui';
|
||||
import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations';
|
||||
|
||||
import { TimelineEventsDetailsItem, TimelineNonEcsData } from '../../../../common';
|
||||
import { useExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions';
|
||||
import { useAlertsActions } from '../alerts_table/timeline_actions/use_alerts_actions';
|
||||
import { useInvestigateInTimeline } from '../alerts_table/timeline_actions/use_investigate_in_timeline';
|
||||
/* Todo: Uncomment case action after getAddToCaseAction is split into action and modal
|
||||
import {
|
||||
ACTION_ADD_TO_CASE
|
||||
} from '../alerts_table/translations';
|
||||
import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana';
|
||||
import { useInsertTimeline } from '../../../cases/components/use_insert_timeline';
|
||||
import { addToCaseActionItem } from './helpers'; */
|
||||
import { useEventFilterAction } from '../alerts_table/timeline_actions/use_event_filter_action';
|
||||
import { useHostIsolationAction } from '../host_isolation/use_host_isolation_action';
|
||||
import { CHANGE_ALERT_STATUS } from './translations';
|
||||
import { getFieldValue } from '../host_isolation/helpers';
|
||||
import type { Ecs } from '../../../../common/ecs';
|
||||
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { endpointAlertCheck } from '../../../common/utils/endpoint_alert_check';
|
||||
|
||||
interface ActionsData {
|
||||
alertStatus: Status;
|
||||
eventId: string;
|
||||
eventKind: string;
|
||||
ruleId: string;
|
||||
ruleName: string;
|
||||
}
|
||||
|
||||
export const TakeActionDropdown = React.memo(
|
||||
({
|
||||
detailsData,
|
||||
ecsData,
|
||||
handleOnEventClosed,
|
||||
isHostIsolationPanelOpen,
|
||||
loadingEventDetails,
|
||||
nonEcsData,
|
||||
onAddEventFilterClick,
|
||||
onAddExceptionTypeClick,
|
||||
onAddIsolationStatusClick,
|
||||
refetch,
|
||||
timelineId,
|
||||
}: {
|
||||
detailsData: TimelineEventsDetailsItem[] | null;
|
||||
ecsData?: Ecs;
|
||||
handleOnEventClosed: () => void;
|
||||
isHostIsolationPanelOpen: boolean;
|
||||
loadingEventDetails: boolean;
|
||||
nonEcsData?: TimelineNonEcsData[];
|
||||
refetch: (() => void) | undefined;
|
||||
onAddEventFilterClick: () => void;
|
||||
onAddExceptionTypeClick: (type: ExceptionListType) => void;
|
||||
onAddIsolationStatusClick: (action: 'isolateHost' | 'unisolateHost') => void;
|
||||
timelineId: string;
|
||||
}) => {
|
||||
/* Todo: Uncomment case action after getAddToCaseAction is split into action and modal
|
||||
const casePermissions = useGetUserCasesPermissions();
|
||||
const { timelines: timelinesUi } = useKibana().services;
|
||||
const insertTimelineHook = useInsertTimeline;
|
||||
*/
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const actionsData = useMemo(
|
||||
() =>
|
||||
[
|
||||
{ category: 'signal', field: 'signal.rule.id', name: 'ruleId' },
|
||||
{ category: 'signal', field: 'signal.rule.name', name: 'ruleName' },
|
||||
{ category: 'signal', field: 'signal.status', name: 'alertStatus' },
|
||||
{ category: 'event', field: 'event.kind', name: 'eventKind' },
|
||||
{ category: '_id', field: '_id', name: 'eventId' },
|
||||
].reduce<ActionsData>(
|
||||
(acc, curr) => ({
|
||||
...acc,
|
||||
[curr.name]: getFieldValue({ category: curr.category, field: curr.field }, detailsData),
|
||||
}),
|
||||
{} as ActionsData
|
||||
),
|
||||
[detailsData]
|
||||
);
|
||||
|
||||
const alertIds = useMemo(() => [actionsData.eventId], [actionsData.eventId]);
|
||||
const isEvent = actionsData.eventKind === 'event';
|
||||
|
||||
const isEndpointAlert = useMemo((): boolean => {
|
||||
if (detailsData == null) {
|
||||
return false;
|
||||
}
|
||||
return endpointAlertCheck({ data: detailsData });
|
||||
}, [detailsData]);
|
||||
|
||||
const togglePopoverHandler = useCallback(() => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
}, [isPopoverOpen]);
|
||||
|
||||
const closePopoverHandler = useCallback(() => {
|
||||
setIsPopoverOpen(false);
|
||||
}, []);
|
||||
|
||||
const closePopoverAndFlyout = useCallback(() => {
|
||||
handleOnEventClosed();
|
||||
setIsPopoverOpen(false);
|
||||
}, [handleOnEventClosed]);
|
||||
|
||||
const handleOnAddIsolationStatusClick = useCallback(
|
||||
(action: 'isolateHost' | 'unisolateHost') => {
|
||||
onAddIsolationStatusClick(action);
|
||||
setIsPopoverOpen(false);
|
||||
},
|
||||
[onAddIsolationStatusClick]
|
||||
);
|
||||
|
||||
const hostIsolationAction = useHostIsolationAction({
|
||||
closePopover: closePopoverHandler,
|
||||
detailsData,
|
||||
onAddIsolationStatusClick: handleOnAddIsolationStatusClick,
|
||||
isHostIsolationPanelOpen,
|
||||
});
|
||||
|
||||
const handleOnAddExceptionTypeClick = useCallback(
|
||||
(type: ExceptionListType) => {
|
||||
onAddExceptionTypeClick(type);
|
||||
setIsPopoverOpen(false);
|
||||
},
|
||||
[onAddExceptionTypeClick]
|
||||
);
|
||||
|
||||
const exceptionActions = useExceptionActions({
|
||||
isEndpointAlert,
|
||||
onAddExceptionTypeClick: handleOnAddExceptionTypeClick,
|
||||
});
|
||||
|
||||
const handleOnAddEventFilterClick = useCallback(() => {
|
||||
onAddEventFilterClick();
|
||||
setIsPopoverOpen(false);
|
||||
}, [onAddEventFilterClick]);
|
||||
|
||||
const eventFilterActions = useEventFilterAction({
|
||||
onAddEventFilterClick: handleOnAddEventFilterClick,
|
||||
});
|
||||
|
||||
const { statusActions } = useAlertsActions({
|
||||
alertStatus: actionsData.alertStatus,
|
||||
eventId: actionsData.eventId,
|
||||
timelineId,
|
||||
closePopover: closePopoverAndFlyout,
|
||||
});
|
||||
|
||||
const { investigateInTimelineAction } = useInvestigateInTimeline({
|
||||
alertIds,
|
||||
ecsRowData: ecsData,
|
||||
onInvestigateInTimelineAlertClick: closePopoverHandler,
|
||||
});
|
||||
|
||||
const alertsActionItems = useMemo(
|
||||
() =>
|
||||
!isEvent && actionsData.ruleId
|
||||
? [
|
||||
{
|
||||
name: CHANGE_ALERT_STATUS,
|
||||
panel: 1,
|
||||
},
|
||||
...exceptionActions,
|
||||
]
|
||||
: [eventFilterActions],
|
||||
[eventFilterActions, exceptionActions, isEvent, actionsData.ruleId]
|
||||
);
|
||||
|
||||
const panels = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 0,
|
||||
items: [
|
||||
...alertsActionItems,
|
||||
/* Todo: Uncomment case action after getAddToCaseAction is split into action and modal
|
||||
...addToCaseActionItem(timelineId),*/
|
||||
...hostIsolationAction,
|
||||
...investigateInTimelineAction,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: CHANGE_ALERT_STATUS,
|
||||
items: statusActions,
|
||||
},
|
||||
/* Todo: Uncomment case action after getAddToCaseAction is split into action and modal
|
||||
{
|
||||
id: 2,
|
||||
title: ACTION_ADD_TO_CASE,
|
||||
content: (
|
||||
<>
|
||||
{ecsData &&
|
||||
timelinesUi.getAddToCaseAction({
|
||||
ecsRowData: ecsData,
|
||||
useInsertTimeline: insertTimelineHook,
|
||||
casePermissions,
|
||||
showIcon: false,
|
||||
})}
|
||||
</>
|
||||
),
|
||||
},*/
|
||||
],
|
||||
[alertsActionItems, hostIsolationAction, investigateInTimelineAction, statusActions]
|
||||
);
|
||||
|
||||
const takeActionButton = useMemo(() => {
|
||||
return (
|
||||
<EuiButton iconSide="right" fill iconType="arrowDown" onClick={togglePopoverHandler}>
|
||||
{TAKE_ACTION}
|
||||
</EuiButton>
|
||||
);
|
||||
}, [togglePopoverHandler]);
|
||||
|
||||
return panels[0].items?.length && !loadingEventDetails ? (
|
||||
<>
|
||||
<EuiPopover
|
||||
id="hostIsolationTakeActionPanel"
|
||||
button={takeActionButton}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopoverHandler}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenu size="s" initialPanelId={0} panels={panels} />
|
||||
</EuiPopover>
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
);
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const CHANGE_ALERT_STATUS = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.takeAction.changeAlertStatus',
|
||||
{
|
||||
defaultMessage: 'Change alert status',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_ADD_ENDPOINT_EXCEPTION = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.takeAction.addEndpointException',
|
||||
{
|
||||
defaultMessage: 'Add Endpoint exception',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_ADD_EXCEPTION = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.takeAction.addException',
|
||||
{
|
||||
defaultMessage: 'Add rule exception',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_ADD_EVENT_FILTER = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.takeAction.addEventFilter',
|
||||
{
|
||||
defaultMessage: 'Add Endpoint event filter',
|
||||
}
|
||||
);
|
||||
|
||||
export const INVESTIGATE_IN_TIMELINE = i18n.translate(
|
||||
'xpack.securitySolution.endpoint.takeAction.investigateInTimeline',
|
||||
{
|
||||
defaultMessage: 'investigate in timeline',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import { SearchResponse } from 'elasticsearch';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import {
|
||||
buildAlertsQuery,
|
||||
formatAlertToEcsSignal,
|
||||
} from '../../../../cases/components/case_view/helpers';
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
|
||||
import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants';
|
||||
import { KibanaServices } from '../../../../common/lib/kibana';
|
||||
|
||||
export const useFetchEcsAlertsData = ({
|
||||
alertIds,
|
||||
skip,
|
||||
onError,
|
||||
}: {
|
||||
alertIds?: string[] | null | undefined;
|
||||
skip?: boolean;
|
||||
onError?: (e: Error) => void;
|
||||
}): { isLoading: boolean | null; alertsEcsData: Ecs[] | null } => {
|
||||
const [isLoading, setIsLoading] = useState<boolean | null>(null);
|
||||
const [alertsEcsData, setAlertEcsData] = useState<Ecs[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
const fetchAlert = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const alertResponse = await KibanaServices.get().http.fetch<
|
||||
SearchResponse<{ '@timestamp': string; [key: string]: unknown }>
|
||||
>(DETECTION_ENGINE_QUERY_SIGNALS_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(buildAlertsQuery(alertIds ?? [])),
|
||||
});
|
||||
|
||||
setAlertEcsData(
|
||||
alertResponse?.hits.hits.reduce<Ecs[]>(
|
||||
(acc, { _id, _index, _source }) => [
|
||||
...acc,
|
||||
{
|
||||
...formatAlertToEcsSignal(_source as {}),
|
||||
_id,
|
||||
_index,
|
||||
timestamp: _source['@timestamp'],
|
||||
},
|
||||
],
|
||||
[]
|
||||
) ?? []
|
||||
);
|
||||
} catch (e) {
|
||||
if (isSubscribed) {
|
||||
if (onError) {
|
||||
onError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isSubscribed) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isEmpty(alertIds) && !skip) {
|
||||
fetchAlert();
|
||||
}
|
||||
|
||||
return (): void => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [alertIds, onError, skip]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
alertsEcsData,
|
||||
};
|
||||
};
|
|
@ -38,6 +38,8 @@ export const useHostIsolationStatus = ({
|
|||
// isMounted tracks if a component is mounted before changing state
|
||||
let isMounted = true;
|
||||
let fleetAgentId: string;
|
||||
setLoading(true);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const metadataResponse = await getHostMetadata({ agentId, signal: abortCtrl.signal });
|
||||
|
@ -73,15 +75,9 @@ export const useHostIsolationStatus = ({
|
|||
}
|
||||
};
|
||||
|
||||
setLoading((prevState) => {
|
||||
if (prevState) {
|
||||
return prevState;
|
||||
}
|
||||
if (!isEmpty(agentId)) {
|
||||
fetchData();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!isEmpty(agentId)) {
|
||||
fetchData();
|
||||
}
|
||||
return () => {
|
||||
// updates to show component is unmounted
|
||||
isMounted = false;
|
||||
|
|
|
@ -261,7 +261,7 @@ Array [
|
|||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 4px 16px 50px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
<EuiFlyout
|
||||
|
@ -454,6 +454,53 @@ Array [
|
|||
</div>
|
||||
</EuiFlyoutBody>
|
||||
</Styled(EuiFlyoutBody)>
|
||||
<Memo()
|
||||
detailsData={null}
|
||||
expandedEvent={
|
||||
Object {
|
||||
"eventId": "my-id",
|
||||
"indexName": "my-index",
|
||||
}
|
||||
}
|
||||
handleOnEventClosed={[Function]}
|
||||
isHostIsolationPanelOpen={false}
|
||||
loadingEventDetails={true}
|
||||
onAddIsolationStatusClick={[Function]}
|
||||
timelineId="test"
|
||||
>
|
||||
<EuiFlyoutFooter>
|
||||
<div
|
||||
className="euiFlyoutFooter"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexEnd"
|
||||
>
|
||||
<div
|
||||
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<Memo()
|
||||
detailsData={null}
|
||||
handleOnEventClosed={[Function]}
|
||||
isHostIsolationPanelOpen={false}
|
||||
loadingEventDetails={true}
|
||||
onAddEventFilterClick={[Function]}
|
||||
onAddExceptionTypeClick={[Function]}
|
||||
onAddIsolationStatusClick={[Function]}
|
||||
timelineId="test"
|
||||
/>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</div>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</EuiFlyoutFooter>
|
||||
</Memo()>
|
||||
</EventDetailsPanelComponent>
|
||||
</div>
|
||||
</EuiFlyout>,
|
||||
|
@ -480,7 +527,7 @@ Array [
|
|||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 4px 16px 50px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
<div
|
||||
|
@ -667,6 +714,53 @@ Array [
|
|||
</div>
|
||||
</EuiFlyoutBody>
|
||||
</Styled(EuiFlyoutBody)>
|
||||
<Memo()
|
||||
detailsData={null}
|
||||
expandedEvent={
|
||||
Object {
|
||||
"eventId": "my-id",
|
||||
"indexName": "my-index",
|
||||
}
|
||||
}
|
||||
handleOnEventClosed={[Function]}
|
||||
isHostIsolationPanelOpen={false}
|
||||
loadingEventDetails={true}
|
||||
onAddIsolationStatusClick={[Function]}
|
||||
timelineId="test"
|
||||
>
|
||||
<EuiFlyoutFooter>
|
||||
<div
|
||||
className="euiFlyoutFooter"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
justifyContent="flexEnd"
|
||||
>
|
||||
<div
|
||||
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
className="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<Memo()
|
||||
detailsData={null}
|
||||
handleOnEventClosed={[Function]}
|
||||
isHostIsolationPanelOpen={false}
|
||||
loadingEventDetails={true}
|
||||
onAddEventFilterClick={[Function]}
|
||||
onAddExceptionTypeClick={[Function]}
|
||||
onAddIsolationStatusClick={[Function]}
|
||||
timelineId="test"
|
||||
/>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</div>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</EuiFlyoutFooter>
|
||||
</Memo()>
|
||||
</EventDetailsPanelComponent>
|
||||
</div>,
|
||||
]
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { find, get } from 'lodash/fp';
|
||||
import { TakeActionDropdown } from '../../../../detections/components/take_action_dropdown';
|
||||
import type { TimelineEventsDetailsItem } from '../../../../../common';
|
||||
import { useExceptionModal } from '../../../../detections/components/alerts_table/timeline_actions/use_add_exception_modal';
|
||||
import { AddExceptionModalWrapper } from '../../../../detections/components/alerts_table/timeline_actions/alert_context_menu';
|
||||
import { EventFiltersModal } from '../../../../management/pages/event_filters/view/components/modal';
|
||||
import { useEventFilterModal } from '../../../../detections/components/alerts_table/timeline_actions/use_event_filter_modal';
|
||||
import { getFieldValue } from '../../../../detections/components/host_isolation/helpers';
|
||||
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { useFetchEcsAlertsData } from '../../../../detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data';
|
||||
|
||||
interface EventDetailsFooterProps {
|
||||
detailsData: TimelineEventsDetailsItem[] | null;
|
||||
expandedEvent: {
|
||||
eventId: string;
|
||||
indexName: string;
|
||||
refetch?: () => void;
|
||||
};
|
||||
handleOnEventClosed: () => void;
|
||||
isHostIsolationPanelOpen: boolean;
|
||||
loadingEventDetails: boolean;
|
||||
onAddIsolationStatusClick: (action: 'isolateHost' | 'unisolateHost') => void;
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
interface AddExceptionModalWrapperData {
|
||||
alertStatus: Status;
|
||||
eventId: string;
|
||||
ruleId: string;
|
||||
ruleName: string;
|
||||
}
|
||||
|
||||
export const EventDetailsFooter = React.memo(
|
||||
({
|
||||
detailsData,
|
||||
expandedEvent,
|
||||
handleOnEventClosed,
|
||||
isHostIsolationPanelOpen,
|
||||
loadingEventDetails,
|
||||
onAddIsolationStatusClick,
|
||||
timelineId,
|
||||
}: EventDetailsFooterProps) => {
|
||||
const ruleIndex = useMemo(
|
||||
() => find({ category: 'signal', field: 'signal.rule.index' }, detailsData)?.values,
|
||||
[detailsData]
|
||||
);
|
||||
|
||||
const addExceptionModalWrapperData = useMemo(
|
||||
() =>
|
||||
[
|
||||
{ category: 'signal', field: 'signal.rule.id', name: 'ruleId' },
|
||||
{ category: 'signal', field: 'signal.rule.name', name: 'ruleName' },
|
||||
{ category: 'signal', field: 'signal.status', name: 'alertStatus' },
|
||||
{ category: '_id', field: '_id', name: 'eventId' },
|
||||
].reduce<AddExceptionModalWrapperData>(
|
||||
(acc, curr) => ({
|
||||
...acc,
|
||||
[curr.name]: getFieldValue({ category: curr.category, field: curr.field }, detailsData),
|
||||
}),
|
||||
{} as AddExceptionModalWrapperData
|
||||
),
|
||||
[detailsData]
|
||||
);
|
||||
|
||||
const eventIds = useMemo(() => [expandedEvent?.eventId], [expandedEvent?.eventId]);
|
||||
|
||||
const {
|
||||
exceptionModalType,
|
||||
onAddExceptionTypeClick,
|
||||
onAddExceptionCancel,
|
||||
onAddExceptionConfirm,
|
||||
ruleIndices,
|
||||
} = useExceptionModal({
|
||||
ruleIndex,
|
||||
refetch: expandedEvent?.refetch,
|
||||
timelineId,
|
||||
});
|
||||
const {
|
||||
closeAddEventFilterModal,
|
||||
isAddEventFilterModalOpen,
|
||||
onAddEventFilterClick,
|
||||
} = useEventFilterModal();
|
||||
|
||||
const { alertsEcsData } = useFetchEcsAlertsData({
|
||||
alertIds: eventIds,
|
||||
skip: expandedEvent?.eventId == null,
|
||||
});
|
||||
|
||||
const ecsData = get(0, alertsEcsData);
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<TakeActionDropdown
|
||||
detailsData={detailsData}
|
||||
handleOnEventClosed={handleOnEventClosed}
|
||||
isHostIsolationPanelOpen={isHostIsolationPanelOpen}
|
||||
loadingEventDetails={loadingEventDetails}
|
||||
onAddEventFilterClick={onAddEventFilterClick}
|
||||
onAddExceptionTypeClick={onAddExceptionTypeClick}
|
||||
onAddIsolationStatusClick={onAddIsolationStatusClick}
|
||||
refetch={expandedEvent?.refetch}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
{/* This is still wrong to do render flyout/modal inside of the flyout
|
||||
We need to completely refactor the EventDetails component to be correct
|
||||
*/}
|
||||
{exceptionModalType != null &&
|
||||
addExceptionModalWrapperData.ruleId != null &&
|
||||
addExceptionModalWrapperData.eventId != null && (
|
||||
<AddExceptionModalWrapper
|
||||
{...addExceptionModalWrapperData}
|
||||
ruleIndices={ruleIndices}
|
||||
exceptionListType={exceptionModalType}
|
||||
onCancel={onAddExceptionCancel}
|
||||
onConfirm={onAddExceptionConfirm}
|
||||
/>
|
||||
)}
|
||||
{isAddEventFilterModalOpen && ecsData != null && (
|
||||
<EventFiltersModal data={ecsData} onCancel={closeAddEventFilterModal} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -5,14 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { find, some } from 'lodash/fp';
|
||||
import { some } from 'lodash/fp';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
|
@ -26,17 +23,16 @@ import { useTimelineEventsDetails } from '../../../containers/details';
|
|||
import { TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import { HostIsolationPanel } from '../../../../detections/components/host_isolation';
|
||||
import { EndpointIsolateSuccess } from '../../../../common/components/endpoint/host_isolation';
|
||||
import { TakeActionDropdown } from '../../../../detections/components/host_isolation/take_action_dropdown';
|
||||
import {
|
||||
ISOLATE_HOST,
|
||||
UNISOLATE_HOST,
|
||||
} from '../../../../detections/components/host_isolation/translations';
|
||||
import { getFieldValue } from '../../../../detections/components/host_isolation/helpers';
|
||||
import { ALERT_DETAILS } from './translations';
|
||||
import { useIsolationPrivileges } from '../../../../common/hooks/endpoint/use_isolate_privileges';
|
||||
import { isIsolationSupported } from '../../../../../common/endpoint/service/host_isolation/utils';
|
||||
import { endpointAlertCheck } from '../../../../common/utils/endpoint_alert_check';
|
||||
import { useWithCaseDetailsRefresh } from '../../../../common/components/endpoint/host_isolation/endpoint_host_isolation_cases_context';
|
||||
import { TimelineEventsDetailsItem } from '../../../../../common';
|
||||
import { TimelineNonEcsData } from '../../../../../common';
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
import { EventDetailsFooter } from './footer';
|
||||
|
||||
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
|
||||
.euiFlyoutBody__overflow {
|
||||
|
@ -47,29 +43,21 @@ const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
|
|||
.euiFlyoutBody__overflowContent {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: ${({ theme }) => `${theme.eui.paddingSizes.xs} ${theme.eui.paddingSizes.m} 50px`};
|
||||
padding: ${({ theme }) => `${theme.eui.paddingSizes.m}`};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const getFieldValue = (
|
||||
{
|
||||
category,
|
||||
field,
|
||||
}: {
|
||||
category: string;
|
||||
field: string;
|
||||
},
|
||||
data: TimelineEventsDetailsItem[] | null
|
||||
) => {
|
||||
const currentField = find({ category, field }, data)?.values;
|
||||
return currentField && currentField.length > 0 ? currentField[0] : '';
|
||||
};
|
||||
|
||||
interface EventDetailsPanelProps {
|
||||
browserFields: BrowserFields;
|
||||
docValueFields: DocValueFields[];
|
||||
expandedEvent: { eventId: string; indexName: string };
|
||||
expandedEvent: {
|
||||
eventId: string;
|
||||
indexName: string;
|
||||
ecsData?: Ecs;
|
||||
nonEcsData?: TimelineNonEcsData[];
|
||||
refetch?: () => void;
|
||||
};
|
||||
handleOnEventClosed: () => void;
|
||||
isFlyoutView?: boolean;
|
||||
tabType: TimelineTabs;
|
||||
|
@ -107,7 +95,6 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
|
|||
setIsIsolateActionSuccessBannerVisible(false);
|
||||
}, []);
|
||||
|
||||
const { isAllowed: isIsolationAllowed } = useIsolationPrivileges();
|
||||
const showHostIsolationPanel = useCallback((action) => {
|
||||
if (action === 'isolateHost' || action === 'unisolateHost') {
|
||||
setIsHostIsolationPanel(true);
|
||||
|
@ -117,30 +104,11 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
|
|||
|
||||
const isAlert = some({ category: 'signal', field: 'signal.rule.id' }, detailsData);
|
||||
|
||||
const isEndpointAlert = useMemo(() => {
|
||||
return endpointAlertCheck({ data: detailsData || [] });
|
||||
}, [detailsData]);
|
||||
|
||||
const ruleName = useMemo(
|
||||
() => getFieldValue({ category: 'signal', field: 'signal.rule.name' }, detailsData),
|
||||
[detailsData]
|
||||
);
|
||||
|
||||
const agentId = useMemo(
|
||||
() => getFieldValue({ category: 'agent', field: 'agent.id' }, detailsData),
|
||||
[detailsData]
|
||||
);
|
||||
|
||||
const hostOsFamily = useMemo(
|
||||
() => getFieldValue({ category: 'host', field: 'host.os.name' }, detailsData),
|
||||
[detailsData]
|
||||
);
|
||||
|
||||
const agentVersion = useMemo(
|
||||
() => getFieldValue({ category: 'agent', field: 'agent.version' }, detailsData),
|
||||
[detailsData]
|
||||
);
|
||||
|
||||
const alertId = useMemo(() => getFieldValue({ category: '_id', field: '_id' }, detailsData), [
|
||||
detailsData,
|
||||
]);
|
||||
|
@ -150,11 +118,6 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
|
|||
[detailsData]
|
||||
);
|
||||
|
||||
const isolationSupported = isIsolationSupported({
|
||||
osName: hostOsFamily,
|
||||
version: agentVersion,
|
||||
});
|
||||
|
||||
const backToAlertDetailsLink = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
|
@ -225,18 +188,16 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
|
|||
/>
|
||||
)}
|
||||
</StyledEuiFlyoutBody>
|
||||
{isIsolationAllowed &&
|
||||
isEndpointAlert &&
|
||||
isolationSupported &&
|
||||
isHostIsolationPanelOpen === false && (
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<TakeActionDropdown onChange={showHostIsolationPanel} agentId={agentId} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
)}
|
||||
|
||||
<EventDetailsFooter
|
||||
detailsData={detailsData}
|
||||
expandedEvent={expandedEvent}
|
||||
handleOnEventClosed={handleOnEventClosed}
|
||||
isHostIsolationPanelOpen={isHostIsolationPanelOpen}
|
||||
loadingEventDetails={loading}
|
||||
onAddIsolationStatusClick={showHostIsolationPanel}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { MouseEvent } from 'react';
|
||||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import { EuiContextMenuItem, EuiButtonIcon, EuiToolTip, EuiText } from '@elastic/eui';
|
||||
|
||||
import { EventsTdContent } from '../../styles';
|
||||
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
|
||||
|
@ -20,6 +20,7 @@ interface ActionIconItemProps {
|
|||
isDisabled?: boolean;
|
||||
onClick?: (event: MouseEvent) => void;
|
||||
children?: React.ReactNode;
|
||||
buttonType?: 'text' | 'icon';
|
||||
}
|
||||
|
||||
const ActionIconItemComponent: React.FC<ActionIconItemProps> = ({
|
||||
|
@ -31,22 +32,41 @@ const ActionIconItemComponent: React.FC<ActionIconItemProps> = ({
|
|||
isDisabled = false,
|
||||
onClick,
|
||||
children,
|
||||
buttonType = 'icon',
|
||||
}) => (
|
||||
<div>
|
||||
<EventsTdContent textAlign="center" width={width}>
|
||||
{children ?? (
|
||||
<EuiToolTip data-test-subj={`${dataTestSubj}-tool-tip`} content={content}>
|
||||
<EuiButtonIcon
|
||||
aria-label={ariaLabel}
|
||||
data-test-subj={`${dataTestSubj}-button`}
|
||||
iconType={iconType}
|
||||
isDisabled={isDisabled}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</EventsTdContent>
|
||||
</div>
|
||||
<>
|
||||
{buttonType === 'icon' && (
|
||||
<div>
|
||||
<EventsTdContent textAlign="center" width={width}>
|
||||
{children ?? (
|
||||
<EuiToolTip data-test-subj={`${dataTestSubj}-tool-tip`} content={content}>
|
||||
<EuiButtonIcon
|
||||
aria-label={ariaLabel}
|
||||
data-test-subj={`${dataTestSubj}-button`}
|
||||
iconType={iconType}
|
||||
isDisabled={isDisabled}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</EventsTdContent>
|
||||
</div>
|
||||
)}
|
||||
{buttonType === 'text' && (
|
||||
<EuiContextMenuItem
|
||||
aria-label={ariaLabel}
|
||||
data-test-subj={`${dataTestSubj}-button-menu-item`}
|
||||
disabled={isDisabled}
|
||||
onClick={onClick}
|
||||
color="text"
|
||||
size="s"
|
||||
>
|
||||
<EuiText data-test-subj={`${dataTestSubj}-button`} size="m">
|
||||
{content}
|
||||
</EuiText>
|
||||
</EuiContextMenuItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
ActionIconItemComponent.displayName = 'ActionIconItemComponent';
|
||||
|
|
|
@ -185,6 +185,7 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
params: {
|
||||
eventId,
|
||||
indexName,
|
||||
refetch,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -199,7 +200,7 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
if (timelineId === TimelineId.active && tabType === TimelineTabs.query) {
|
||||
activeTimeline.toggleExpandedDetail({ ...updatedExpandedDetail });
|
||||
}
|
||||
}, [dispatch, event._id, event._index, tabType, timelineId]);
|
||||
}, [dispatch, event._id, event._index, refetch, tabType, timelineId]);
|
||||
|
||||
const associateNote = useCallback(
|
||||
(noteId: string) => {
|
||||
|
|
|
@ -68,6 +68,7 @@ jest.mock('../../../../common/lib/helpers/scheduler', () => ({
|
|||
|
||||
describe('Body', () => {
|
||||
const mount = useMountAppended();
|
||||
const mockRefetch = jest.fn();
|
||||
const props: StatefulBodyProps = {
|
||||
activePage: 0,
|
||||
browserFields: mockBrowserFields,
|
||||
|
@ -80,7 +81,7 @@ describe('Body', () => {
|
|||
isSelectAllChecked: false,
|
||||
loadingEventIds: [],
|
||||
pinnedEventIds: {},
|
||||
refetch: jest.fn(),
|
||||
refetch: mockRefetch,
|
||||
renderCellValue: DefaultCellRenderer,
|
||||
rowRenderers: defaultRowRenderers,
|
||||
selectedEventIds: {},
|
||||
|
@ -253,6 +254,7 @@ describe('Body', () => {
|
|||
params: {
|
||||
eventId: '1',
|
||||
indexName: undefined,
|
||||
refetch: mockRefetch,
|
||||
},
|
||||
tabType: 'query',
|
||||
timelineId: 'timeline-test',
|
||||
|
@ -277,6 +279,7 @@ describe('Body', () => {
|
|||
params: {
|
||||
eventId: '1',
|
||||
indexName: undefined,
|
||||
refetch: mockRefetch,
|
||||
},
|
||||
tabType: 'pinned',
|
||||
timelineId: 'timeline-test',
|
||||
|
@ -301,6 +304,7 @@ describe('Body', () => {
|
|||
params: {
|
||||
eventId: '1',
|
||||
indexName: undefined,
|
||||
refetch: mockRefetch,
|
||||
},
|
||||
tabType: 'notes',
|
||||
timelineId: 'timeline-test',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue