mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Security Solution][Alert details] - finish cleanup of old event_details folder (#190119)
This commit is contained in:
parent
6b6bb3cfe0
commit
0cc275a5df
45 changed files with 295 additions and 1128 deletions
|
@ -11,7 +11,7 @@ import { css } from '@emotion/react';
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AttackChain } from '../../../attack/attack_chain';
|
||||
import { InvestigateInTimelineButton } from '../../../../common/components/event_details/table/investigate_in_timeline_button';
|
||||
import { InvestigateInTimelineButton } from '../../../../common/components/event_details/investigate_in_timeline_button';
|
||||
import { buildAlertsKqlFilter } from '../../../../detections/components/alerts_table/actions';
|
||||
import { getTacticMetadata } from '../../../helpers';
|
||||
import { AttackDiscoveryMarkdownFormatter } from '../../../attack_discovery_markdown_formatter';
|
||||
|
|
|
@ -1,11 +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.
|
||||
*/
|
||||
|
||||
export enum EventsViewType {
|
||||
osqueryView = 'osquery-results-view',
|
||||
responseActionsView = 'response-actions-results-view',
|
||||
}
|
|
@ -1,95 +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 type { UseSummaryRowsProps } from './get_alert_summary_rows';
|
||||
import { useSummaryRows } from './get_alert_summary_rows';
|
||||
import { createAppRootMockRenderer, endpointAlertDataMock } from '../../mock/endpoint';
|
||||
import type { RenderHookResult } from '@testing-library/react-hooks/src/types';
|
||||
import type { AlertSummaryRow } from './helpers';
|
||||
|
||||
describe('useSummaryRows', () => {
|
||||
let hookProps: UseSummaryRowsProps;
|
||||
let renderHook: () => RenderHookResult<UseSummaryRowsProps, AlertSummaryRow[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
const appContextMock = createAppRootMockRenderer();
|
||||
|
||||
appContextMock.setExperimentalFlag({
|
||||
responseActionsSentinelOneV1Enabled: true,
|
||||
responseActionsCrowdstrikeManualHostIsolationEnabled: true,
|
||||
});
|
||||
|
||||
hookProps = {
|
||||
data: endpointAlertDataMock.generateEndpointAlertDetailsItemData(),
|
||||
browserFields: {},
|
||||
scopeId: 'scope-id',
|
||||
eventId: 'event-id',
|
||||
investigationFields: [],
|
||||
};
|
||||
|
||||
renderHook = () => {
|
||||
return appContextMock.renderHook<UseSummaryRowsProps, AlertSummaryRow[]>(() =>
|
||||
useSummaryRows(hookProps)
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
it('returns summary rows for default event categories', () => {
|
||||
const { result } = renderHook();
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ title: 'host.name', description: expect.anything() }),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('excludes fields not related to the event source', () => {
|
||||
const { result } = renderHook();
|
||||
|
||||
expect(result.current).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: 'agent.id',
|
||||
description: expect.anything(),
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('includes sentinel_one agent status field', () => {
|
||||
hookProps.data = endpointAlertDataMock.generateSentinelOneAlertDetailsItemData();
|
||||
const { result } = renderHook();
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: 'Agent status',
|
||||
description: expect.objectContaining({
|
||||
values: ['abfe4a35-d5b4-42a0-a539-bd054c791769'],
|
||||
}),
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('includes crowdstrike agent status field', () => {
|
||||
hookProps.data = endpointAlertDataMock.generateCrowdStrikeAlertDetailsItemData();
|
||||
const { result } = renderHook();
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: 'Agent status',
|
||||
description: expect.objectContaining({
|
||||
values: ['abfe4a35-d5b4-42a0-a539-bd054c791769'],
|
||||
}),
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
|
@ -4,16 +4,12 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import { find, isEmpty, uniqBy } from 'lodash/fp';
|
||||
import { find, uniqBy } from 'lodash/fp';
|
||||
import { ALERT_RULE_PARAMETERS, ALERT_RULE_TYPE } from '@kbn/rule-data-utils';
|
||||
|
||||
import { EventCode, EventCategory } from '@kbn/securitysolution-ecs';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SUPPORTED_AGENT_ID_ALERT_FIELDS } from '../../../../common/endpoint/service/response_actions/constants';
|
||||
import { isResponseActionsAlertAgentIdField } from '../../lib/endpoint';
|
||||
import { useAlertResponseActionsSupport } from '../../hooks/endpoint/use_alert_response_actions_support';
|
||||
import * as i18n from './translations';
|
||||
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
|
||||
import {
|
||||
ALERTS_HEADERS_THRESHOLD_CARDINALITY,
|
||||
ALERTS_HEADERS_THRESHOLD_COUNT,
|
||||
|
@ -31,17 +27,27 @@ import {
|
|||
AGENT_STATUS_FIELD_NAME,
|
||||
QUARANTINED_PATH_FIELD_NAME,
|
||||
} from '../../../timelines/components/timeline/body/renderers/constants';
|
||||
import type { AlertSummaryRow } from './helpers';
|
||||
import { getEnrichedFieldInfo } from './helpers';
|
||||
import type { EventSummaryField, EnrichedFieldInfo } from './types';
|
||||
import type { EventSummaryField } from './types';
|
||||
import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
|
||||
|
||||
const THRESHOLD_TERMS_FIELD = `${ALERT_THRESHOLD_RESULT}.terms.field`;
|
||||
const THRESHOLD_TERMS_VALUE = `${ALERT_THRESHOLD_RESULT}.terms.value`;
|
||||
const THRESHOLD_CARDINALITY_FIELD = `${ALERT_THRESHOLD_RESULT}.cardinality.field`;
|
||||
const THRESHOLD_CARDINALITY_VALUE = `${ALERT_THRESHOLD_RESULT}.cardinality.value`;
|
||||
const THRESHOLD_COUNT = `${ALERT_THRESHOLD_RESULT}.count`;
|
||||
|
||||
const AGENT_STATUS = i18n.translate('xpack.securitySolution.detections.alerts.agentStatus', {
|
||||
defaultMessage: 'Agent status',
|
||||
});
|
||||
const QUARANTINED_FILE_PATH = i18n.translate(
|
||||
'xpack.securitySolution.detections.alerts.quarantinedFilePath',
|
||||
{
|
||||
defaultMessage: 'Quarantined file path',
|
||||
}
|
||||
);
|
||||
const RULE_TYPE = i18n.translate('xpack.securitySolution.detections.alerts.ruleType', {
|
||||
defaultMessage: 'Rule type',
|
||||
});
|
||||
|
||||
/** Always show these fields */
|
||||
const alwaysDisplayedFields: EventSummaryField[] = [
|
||||
{ id: 'host.name' },
|
||||
|
@ -52,7 +58,7 @@ const alwaysDisplayedFields: EventSummaryField[] = [
|
|||
return {
|
||||
id: fieldPath,
|
||||
overrideField: AGENT_STATUS_FIELD_NAME,
|
||||
label: i18n.AGENT_STATUS,
|
||||
label: AGENT_STATUS,
|
||||
};
|
||||
}),
|
||||
|
||||
|
@ -72,7 +78,7 @@ const alwaysDisplayedFields: EventSummaryField[] = [
|
|||
{ id: 'orchestrator.resource.type' },
|
||||
{ id: 'process.executable' },
|
||||
{ id: 'file.path' },
|
||||
{ id: ALERT_RULE_TYPE, label: i18n.RULE_TYPE },
|
||||
{ id: ALERT_RULE_TYPE, label: RULE_TYPE },
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -163,7 +169,7 @@ function getFieldsByEventCode(
|
|||
{
|
||||
id: 'file.Ext.quarantine_path',
|
||||
overrideField: QUARANTINED_PATH_FIELD_NAME,
|
||||
label: i18n.QUARANTINED_FILE_PATH,
|
||||
label: QUARANTINED_FILE_PATH,
|
||||
},
|
||||
];
|
||||
default:
|
||||
|
@ -297,192 +303,3 @@ export function getEventCategoriesFromData(data: TimelineEventsDetailsItem[]): E
|
|||
|
||||
return { primaryEventCategory, allEventCategories };
|
||||
}
|
||||
|
||||
export interface UseSummaryRowsProps {
|
||||
data: TimelineEventsDetailsItem[];
|
||||
browserFields: BrowserFields;
|
||||
scopeId: string;
|
||||
eventId: string;
|
||||
investigationFields?: string[];
|
||||
isDraggable?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
export const useSummaryRows = ({
|
||||
data,
|
||||
browserFields,
|
||||
scopeId,
|
||||
eventId,
|
||||
isDraggable = false,
|
||||
isReadOnly = false,
|
||||
investigationFields,
|
||||
}: UseSummaryRowsProps): AlertSummaryRow[] => {
|
||||
const responseActionsSupport = useAlertResponseActionsSupport(data);
|
||||
|
||||
return useMemo(() => {
|
||||
const eventCategories = getEventCategoriesFromData(data);
|
||||
|
||||
const eventCodeField = find({ category: 'event', field: 'event.code' }, data);
|
||||
|
||||
const eventCode = Array.isArray(eventCodeField?.originalValue)
|
||||
? eventCodeField?.originalValue?.[0]
|
||||
: eventCodeField?.originalValue;
|
||||
|
||||
const eventRuleTypeField = find({ category: 'kibana', field: ALERT_RULE_TYPE }, data);
|
||||
const eventRuleType = Array.isArray(eventRuleTypeField?.originalValue)
|
||||
? eventRuleTypeField?.originalValue?.[0]
|
||||
: eventRuleTypeField?.originalValue;
|
||||
|
||||
const tableFields = getEventFieldsToDisplay({
|
||||
eventCategories,
|
||||
eventCode,
|
||||
eventRuleType,
|
||||
highlightedFieldsOverride: investigationFields ?? [],
|
||||
});
|
||||
|
||||
return data != null
|
||||
? tableFields.reduce<AlertSummaryRow[]>((acc, field) => {
|
||||
const item = data.find(
|
||||
(d) => d.field === field.id || (field.legacyId && d.field === field.legacyId)
|
||||
);
|
||||
if (!item || isEmpty(item.values)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// If we found the data by its legacy id we swap the ids to display the correct one
|
||||
if (item.field === field.legacyId) {
|
||||
field.id = field.legacyId;
|
||||
}
|
||||
|
||||
const linkValueField =
|
||||
field.linkField != null && data.find((d) => d.field === field.linkField);
|
||||
const description = {
|
||||
...getEnrichedFieldInfo({
|
||||
item,
|
||||
linkValueField: linkValueField || undefined,
|
||||
contextId: scopeId,
|
||||
scopeId,
|
||||
browserFields,
|
||||
eventId,
|
||||
field,
|
||||
}),
|
||||
isDraggable,
|
||||
isReadOnly,
|
||||
};
|
||||
|
||||
// If the field is one used by a supported Response Actions agentType,
|
||||
// and the alert's host supports response actions
|
||||
// but the alert field is not the one that the agentType on the alert host uses,
|
||||
// then exit and return accumulator
|
||||
if (
|
||||
isResponseActionsAlertAgentIdField(field.id) &&
|
||||
responseActionsSupport.isSupported &&
|
||||
responseActionsSupport.details.agentIdField !== field.id
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (field.id === THRESHOLD_TERMS_FIELD) {
|
||||
const enrichedInfo = enrichThresholdTerms(item, data, description);
|
||||
if (enrichedInfo) {
|
||||
return [...acc, ...enrichedInfo];
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
|
||||
if (field.id === THRESHOLD_CARDINALITY_FIELD) {
|
||||
const enrichedInfo = enrichThresholdCardinality(item, data, description);
|
||||
if (enrichedInfo) {
|
||||
return [...acc, enrichedInfo];
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
title: field.label ?? field.id,
|
||||
description,
|
||||
},
|
||||
];
|
||||
}, [])
|
||||
: [];
|
||||
}, [
|
||||
data,
|
||||
investigationFields,
|
||||
scopeId,
|
||||
browserFields,
|
||||
eventId,
|
||||
isDraggable,
|
||||
isReadOnly,
|
||||
responseActionsSupport.details.agentIdField,
|
||||
responseActionsSupport.isSupported,
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Enriches the summary data for threshold terms.
|
||||
* For any given threshold term, it generates a row with the term's name and the associated value.
|
||||
*/
|
||||
function enrichThresholdTerms(
|
||||
{ values: termsFieldArr }: TimelineEventsDetailsItem,
|
||||
data: TimelineEventsDetailsItem[],
|
||||
description: EnrichedFieldInfo
|
||||
) {
|
||||
const termsValueItem = data.find((d) => d.field === THRESHOLD_TERMS_VALUE);
|
||||
const termsValueArray = termsValueItem && termsValueItem.values;
|
||||
|
||||
// Make sure both `fields` and `values` are an array and that they have the same length
|
||||
if (
|
||||
Array.isArray(termsFieldArr) &&
|
||||
termsFieldArr.length > 0 &&
|
||||
Array.isArray(termsValueArray) &&
|
||||
termsFieldArr.length === termsValueArray.length
|
||||
) {
|
||||
return termsFieldArr
|
||||
.map((field, index) => ({
|
||||
title: field,
|
||||
description: {
|
||||
...description,
|
||||
values: [termsValueArray[index]],
|
||||
},
|
||||
}))
|
||||
.filter(
|
||||
(entry) =>
|
||||
!alwaysDisplayedFields
|
||||
.map((alwaysThereEntry) => alwaysThereEntry.id)
|
||||
.includes(entry.title)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enriches the summary data for threshold cardinality.
|
||||
* Reads out the cardinality field and the value and interpolates them into a combined string value.
|
||||
*/
|
||||
function enrichThresholdCardinality(
|
||||
{ values: cardinalityFieldArr }: TimelineEventsDetailsItem,
|
||||
data: TimelineEventsDetailsItem[],
|
||||
description: EnrichedFieldInfo
|
||||
) {
|
||||
const cardinalityValueItem = data.find((d) => d.field === THRESHOLD_CARDINALITY_VALUE);
|
||||
const cardinalityValueArray = cardinalityValueItem && cardinalityValueItem.values;
|
||||
|
||||
// Only return a summary row if we actually have the correct field and value
|
||||
if (
|
||||
Array.isArray(cardinalityFieldArr) &&
|
||||
cardinalityFieldArr.length === 1 &&
|
||||
Array.isArray(cardinalityValueArray) &&
|
||||
cardinalityFieldArr.length === cardinalityValueArray.length
|
||||
) {
|
||||
return {
|
||||
title: ALERTS_HEADERS_THRESHOLD_CARDINALITY,
|
||||
description: {
|
||||
...description,
|
||||
values: [`count(${cardinalityFieldArr[0]}) >= ${cardinalityValueArray[0]}`],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,42 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { get, getOr, isEmpty } from 'lodash/fp';
|
||||
|
||||
import {
|
||||
elementOrChildrenHasFocus,
|
||||
getFocusedDataColindexCell,
|
||||
getTableSkipFocus,
|
||||
handleSkipFocus,
|
||||
stopPropagationAndPreventDefault,
|
||||
} from '@kbn/timelines-plugin/public';
|
||||
import type { BrowserFields } from '../../containers/source';
|
||||
import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
|
||||
import type { EnrichedFieldInfo, EventSummaryField } from './types';
|
||||
|
||||
import {
|
||||
AGENT_STATUS_FIELD_NAME,
|
||||
QUARANTINED_PATH_FIELD_NAME,
|
||||
} from '../../../timelines/components/timeline/body/renderers/constants';
|
||||
|
||||
/**
|
||||
* An item rendered in the table
|
||||
*/
|
||||
export interface Item {
|
||||
description: string;
|
||||
field: JSX.Element;
|
||||
fieldId: string;
|
||||
type: string;
|
||||
values: string[];
|
||||
}
|
||||
|
||||
export interface AlertSummaryRow {
|
||||
title: string;
|
||||
description: EnrichedFieldInfo & {
|
||||
isDraggable?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
};
|
||||
}
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
|
||||
/** Returns example text, or an empty string if the field does not have an example */
|
||||
export const getExampleText = (example: string | number | null | undefined): string =>
|
||||
|
@ -69,111 +34,3 @@ export const getIconFromType = (type: string | null | undefined) => {
|
|||
};
|
||||
|
||||
export const EVENT_FIELDS_TABLE_CLASS_NAME = 'event-fields-table';
|
||||
|
||||
/**
|
||||
* Returns `true` if the Event Details "event fields" table, or it's children,
|
||||
* has focus
|
||||
*/
|
||||
export const tableHasFocus = (containerElement: HTMLElement | null): boolean =>
|
||||
elementOrChildrenHasFocus(
|
||||
containerElement?.querySelector<HTMLDivElement>(`.${EVENT_FIELDS_TABLE_CLASS_NAME}`)
|
||||
);
|
||||
|
||||
/**
|
||||
* This function has a side effect. It will skip focus "after" or "before"
|
||||
* the Event Details table, with exceptions as noted below.
|
||||
*
|
||||
* If the currently-focused table cell has additional focusable children,
|
||||
* i.e. draggables or always-open popover content, the browser's "natural"
|
||||
* focus management will determine which element is focused next.
|
||||
*/
|
||||
export const onEventDetailsTabKeyPressed = ({
|
||||
containerElement,
|
||||
keyboardEvent,
|
||||
onSkipFocusBeforeEventsTable,
|
||||
onSkipFocusAfterEventsTable,
|
||||
}: {
|
||||
containerElement: HTMLElement | null;
|
||||
keyboardEvent: React.KeyboardEvent;
|
||||
onSkipFocusBeforeEventsTable: () => void;
|
||||
onSkipFocusAfterEventsTable: () => void;
|
||||
}) => {
|
||||
const { shiftKey } = keyboardEvent;
|
||||
|
||||
const eventFieldsTableSkipFocus = getTableSkipFocus({
|
||||
containerElement,
|
||||
getFocusedCell: getFocusedDataColindexCell,
|
||||
shiftKey,
|
||||
tableHasFocus,
|
||||
tableClassName: EVENT_FIELDS_TABLE_CLASS_NAME,
|
||||
});
|
||||
|
||||
if (eventFieldsTableSkipFocus !== 'SKIP_FOCUS_NOOP') {
|
||||
stopPropagationAndPreventDefault(keyboardEvent);
|
||||
handleSkipFocus({
|
||||
onSkipFocusBackwards: onSkipFocusBeforeEventsTable,
|
||||
onSkipFocusForward: onSkipFocusAfterEventsTable,
|
||||
skipFocus: eventFieldsTableSkipFocus,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export function getEnrichedFieldInfo({
|
||||
browserFields,
|
||||
contextId,
|
||||
eventId,
|
||||
field,
|
||||
item,
|
||||
linkValueField,
|
||||
scopeId,
|
||||
}: {
|
||||
browserFields: BrowserFields;
|
||||
contextId: string;
|
||||
item: TimelineEventsDetailsItem;
|
||||
eventId: string;
|
||||
field?: EventSummaryField;
|
||||
scopeId: string;
|
||||
linkValueField?: TimelineEventsDetailsItem;
|
||||
}): EnrichedFieldInfo {
|
||||
const fieldInfo = {
|
||||
contextId,
|
||||
eventId,
|
||||
fieldType: 'string',
|
||||
linkValue: undefined,
|
||||
scopeId,
|
||||
};
|
||||
const linkValue = getOr(null, 'originalValue.0', linkValueField);
|
||||
const category = item.category ?? '';
|
||||
const fieldName = item.field ?? '';
|
||||
|
||||
const browserField = get([category, 'fields', fieldName], browserFields);
|
||||
const overrideField = field?.overrideField;
|
||||
return {
|
||||
...fieldInfo,
|
||||
data: {
|
||||
field: overrideField ?? fieldName,
|
||||
format: browserField?.format?.id ?? '',
|
||||
type: browserField?.type ?? '',
|
||||
isObjectArray: item.isObjectArray,
|
||||
},
|
||||
values: item.values,
|
||||
linkValue: linkValue ?? undefined,
|
||||
fieldFromBrowserField: browserField,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A lookup table for fields that should not have actions
|
||||
*/
|
||||
export const FIELDS_WITHOUT_ACTIONS: { [field: string]: boolean } = {
|
||||
[AGENT_STATUS_FIELD_NAME]: true,
|
||||
[QUARANTINED_PATH_FIELD_NAME]: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether the given field should have hover or row actions.
|
||||
* The lookup is fast, so it is not necessary to memoize the result.
|
||||
*/
|
||||
export function hasHoverOrRowActions(field: string): boolean {
|
||||
return !FIELDS_WITHOUT_ACTIONS[field];
|
||||
}
|
||||
|
|
|
@ -1,16 +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 { i18n } from '@kbn/i18n';
|
||||
|
||||
// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462
|
||||
export const SUPPRESSED_ALERTS_COUNT_TECHNICAL_PREVIEW = i18n.translate(
|
||||
'xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCountTechnicalPreview',
|
||||
{
|
||||
defaultMessage: 'Technical Preview',
|
||||
}
|
||||
);
|
|
@ -9,12 +9,12 @@ import { render, screen } from '@testing-library/react';
|
|||
import React from 'react';
|
||||
|
||||
import { InvestigateInTimelineButton } from './investigate_in_timeline_button';
|
||||
import { TestProviders } from '../../../mock';
|
||||
import { TestProviders } from '../../mock';
|
||||
import { getDataProvider } from './use_action_cell_data_provider';
|
||||
|
||||
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations';
|
||||
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../detections/components/alerts_table/translations';
|
||||
|
||||
jest.mock('../../../lib/kibana');
|
||||
jest.mock('../../lib/kibana');
|
||||
|
||||
describe('InvestigateInTimelineButton', () => {
|
||||
describe('When all props are provided', () => {
|
|
@ -12,18 +12,18 @@ import type { IconType } from '@elastic/eui';
|
|||
import type { Filter } from '@kbn/es-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { sourcererSelectors } from '../../../store';
|
||||
import { InputsModelId } from '../../../store/inputs/constants';
|
||||
import type { TimeRange } from '../../../store/inputs/model';
|
||||
import { inputsActions } from '../../../store/inputs';
|
||||
import { updateProviders, setFilters } from '../../../../timelines/store/actions';
|
||||
import { sourcererActions } from '../../../store/actions';
|
||||
import { SourcererScopeName } from '../../../../sourcerer/store/model';
|
||||
import type { DataProvider } from '../../../../../common/types';
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
import { TimelineTypeEnum } from '../../../../../common/api/timeline';
|
||||
import { useCreateTimeline } from '../../../../timelines/hooks/use_create_timeline';
|
||||
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations';
|
||||
import { sourcererSelectors } from '../../store';
|
||||
import { InputsModelId } from '../../store/inputs/constants';
|
||||
import type { TimeRange } from '../../store/inputs/model';
|
||||
import { inputsActions } from '../../store/inputs';
|
||||
import { updateProviders, setFilters } from '../../../timelines/store/actions';
|
||||
import { sourcererActions } from '../../store/actions';
|
||||
import { SourcererScopeName } from '../../../sourcerer/store/model';
|
||||
import type { DataProvider } from '../../../../common/types';
|
||||
import { TimelineId } from '../../../../common/types/timeline';
|
||||
import { TimelineTypeEnum } from '../../../../common/api/timeline';
|
||||
import { useCreateTimeline } from '../../../timelines/hooks/use_create_timeline';
|
||||
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../detections/components/alerts_table/translations';
|
||||
|
||||
export interface InvestigateInTimelineButtonProps {
|
||||
asEmptyButton: boolean;
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const generateAlertDetailsDataMock = () => [
|
||||
const generateAlertDetailsDataMock = () => [
|
||||
{ category: 'process', field: 'process.name', values: ['-'], originalValue: '-' },
|
||||
{ category: 'process', field: 'process.pid', values: [0], originalValue: 0 },
|
||||
{ category: 'process', field: 'process.executable', values: ['-'], originalValue: '-' },
|
|
@ -1,196 +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.
|
||||
*/
|
||||
|
||||
/* eslint-disable complexity */
|
||||
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { escapeDataProviderId } from '@kbn/securitysolution-t-grid';
|
||||
import { isArray, isEmpty, isString } from 'lodash/fp';
|
||||
import { useMemo } from 'react';
|
||||
import type { FieldSpec } from '@kbn/data-plugin/common';
|
||||
|
||||
import {
|
||||
AGENT_STATUS_FIELD_NAME,
|
||||
EVENT_MODULE_FIELD_NAME,
|
||||
EVENT_URL_FIELD_NAME,
|
||||
GEO_FIELD_TYPE,
|
||||
HOST_NAME_FIELD_NAME,
|
||||
IP_FIELD_TYPE,
|
||||
MESSAGE_FIELD_NAME,
|
||||
REFERENCE_URL_FIELD_NAME,
|
||||
RULE_REFERENCE_FIELD_NAME,
|
||||
SIGNAL_RULE_NAME_FIELD_NAME,
|
||||
SIGNAL_STATUS_FIELD_NAME,
|
||||
} from '../../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { BYTES_FORMAT } from '../../../../timelines/components/timeline/body/renderers/bytes';
|
||||
import { EVENT_DURATION_FIELD_NAME } from '../../../../timelines/components/duration';
|
||||
import { getDisplayValue } from '../../../../timelines/components/timeline/data_providers/helpers';
|
||||
import { PORT_NAMES } from '../../../../explore/network/components/port/helpers';
|
||||
import { INDICATOR_REFERENCE } from '../../../../../common/cti/constants';
|
||||
import type { DataProvider, DataProvidersAnd, QueryOperator } from '../../../../../common/types';
|
||||
import { IS_OPERATOR } from '../../../../../common/types';
|
||||
|
||||
export interface UseActionCellDataProvider {
|
||||
contextId?: string;
|
||||
eventId?: string;
|
||||
field: string;
|
||||
fieldFormat?: string;
|
||||
fieldFromBrowserField?: Partial<FieldSpec>;
|
||||
fieldType?: string;
|
||||
isObjectArray?: boolean;
|
||||
linkValue?: string | null;
|
||||
values: string[] | null | undefined;
|
||||
}
|
||||
|
||||
export interface ActionCellValuesAndDataProvider {
|
||||
values: string[];
|
||||
dataProviders: DataProvider[];
|
||||
filters: Filter[];
|
||||
}
|
||||
|
||||
export const getDataProvider = (
|
||||
field: string,
|
||||
id: string,
|
||||
value: string | string[],
|
||||
operator: QueryOperator = IS_OPERATOR,
|
||||
excluded: boolean = false
|
||||
): DataProvider => ({
|
||||
and: [],
|
||||
enabled: true,
|
||||
id: escapeDataProviderId(id),
|
||||
name: field,
|
||||
excluded,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field,
|
||||
value,
|
||||
operator,
|
||||
displayValue: getDisplayValue(value),
|
||||
},
|
||||
});
|
||||
|
||||
export const getDataProviderAnd = (
|
||||
field: string,
|
||||
id: string,
|
||||
value: string | string[],
|
||||
operator: QueryOperator = IS_OPERATOR,
|
||||
excluded: boolean = false
|
||||
): DataProvidersAnd => {
|
||||
const { and, ...dataProvider } = getDataProvider(field, id, value, operator, excluded);
|
||||
return dataProvider;
|
||||
};
|
||||
|
||||
export const useActionCellDataProvider = ({
|
||||
contextId,
|
||||
eventId,
|
||||
field,
|
||||
fieldFormat,
|
||||
fieldFromBrowserField,
|
||||
fieldType,
|
||||
isObjectArray,
|
||||
linkValue,
|
||||
values,
|
||||
}: UseActionCellDataProvider): ActionCellValuesAndDataProvider | null => {
|
||||
const cellData = useMemo(() => {
|
||||
if (values === null || values === undefined) return null;
|
||||
const arrayValues = Array.isArray(values) ? values : [values];
|
||||
|
||||
// For fields with multiple values we need add an extra filter that makes sure
|
||||
// that only fields that match ALL the values are queried later on.
|
||||
let filters: Filter[] = [];
|
||||
if (arrayValues.length > 1) {
|
||||
filters = [
|
||||
{
|
||||
meta: {},
|
||||
query: {
|
||||
bool: {
|
||||
must: arrayValues.map((value) => ({ term: { [field]: value } })),
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return arrayValues.reduce<ActionCellValuesAndDataProvider>(
|
||||
(memo, value, index) => {
|
||||
let id: string = '';
|
||||
let valueAsString: string = isString(value) ? value : `${values}`;
|
||||
const appendedUniqueId = `${contextId}-${eventId}-${field}-${index}-${value}`;
|
||||
if (fieldFromBrowserField == null) {
|
||||
memo.values.push(valueAsString);
|
||||
return memo;
|
||||
}
|
||||
|
||||
if (isObjectArray || fieldType === GEO_FIELD_TYPE || [MESSAGE_FIELD_NAME].includes(field)) {
|
||||
memo.values.push(valueAsString);
|
||||
return memo;
|
||||
} else if (fieldType === IP_FIELD_TYPE) {
|
||||
id = `formatted-ip-data-provider-${contextId}-${field}-${value}-${eventId}`;
|
||||
if (isString(value) && !isEmpty(value)) {
|
||||
let addresses = value;
|
||||
try {
|
||||
addresses = JSON.parse(value);
|
||||
} catch (_) {
|
||||
// Default to keeping the existing string value
|
||||
}
|
||||
if (isArray(addresses)) {
|
||||
valueAsString = addresses.join(',');
|
||||
addresses.forEach((ip) => memo.dataProviders.push(getDataProvider(field, id, ip)));
|
||||
}
|
||||
memo.dataProviders.push(getDataProvider(field, id, addresses));
|
||||
memo.values.push(valueAsString);
|
||||
return memo;
|
||||
}
|
||||
} else if (PORT_NAMES.some((portName) => field === portName)) {
|
||||
id = `port-default-draggable-${appendedUniqueId}`;
|
||||
} else if (field === EVENT_DURATION_FIELD_NAME) {
|
||||
id = `duration-default-draggable-${appendedUniqueId}`;
|
||||
} else if (field === HOST_NAME_FIELD_NAME) {
|
||||
id = `event-details-value-default-draggable-${appendedUniqueId}`;
|
||||
} else if (fieldFormat === BYTES_FORMAT) {
|
||||
id = `bytes-default-draggable-${appendedUniqueId}`;
|
||||
} else if (field === SIGNAL_RULE_NAME_FIELD_NAME) {
|
||||
id = `event-details-value-default-draggable-${appendedUniqueId}-${linkValue}`;
|
||||
} else if (field === EVENT_MODULE_FIELD_NAME) {
|
||||
id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`;
|
||||
} else if (field === SIGNAL_STATUS_FIELD_NAME) {
|
||||
id = `alert-details-value-default-draggable-${appendedUniqueId}`;
|
||||
} else if (field === AGENT_STATUS_FIELD_NAME) {
|
||||
const valueToUse = typeof value === 'string' ? value : '';
|
||||
id = `event-details-value-default-draggable-${appendedUniqueId}`;
|
||||
valueAsString = valueToUse;
|
||||
} else if (
|
||||
[
|
||||
RULE_REFERENCE_FIELD_NAME,
|
||||
REFERENCE_URL_FIELD_NAME,
|
||||
EVENT_URL_FIELD_NAME,
|
||||
INDICATOR_REFERENCE,
|
||||
].includes(field)
|
||||
) {
|
||||
id = `event-details-value-default-draggable-${appendedUniqueId}-${value}`;
|
||||
} else {
|
||||
id = `event-details-value-default-draggable-${appendedUniqueId}`;
|
||||
}
|
||||
memo.values.push(valueAsString);
|
||||
memo.dataProviders.push(getDataProvider(field, id, value));
|
||||
return memo;
|
||||
},
|
||||
{ values: [], dataProviders: [], filters }
|
||||
);
|
||||
}, [
|
||||
contextId,
|
||||
eventId,
|
||||
field,
|
||||
fieldFormat,
|
||||
fieldFromBrowserField,
|
||||
fieldType,
|
||||
isObjectArray,
|
||||
linkValue,
|
||||
values,
|
||||
]);
|
||||
return cellData;
|
||||
};
|
|
@ -7,64 +7,6 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const TABLE = i18n.translate('xpack.securitySolution.eventDetails.table', {
|
||||
defaultMessage: 'Table',
|
||||
});
|
||||
|
||||
export const DESCRIPTION = i18n.translate('xpack.securitySolution.eventDetails.description', {
|
||||
defaultMessage: 'Description',
|
||||
});
|
||||
|
||||
export const AGENT_STATUS = i18n.translate('xpack.securitySolution.detections.alerts.agentStatus', {
|
||||
defaultMessage: 'Agent status',
|
||||
});
|
||||
|
||||
export const QUARANTINED_FILE_PATH = i18n.translate(
|
||||
'xpack.securitySolution.detections.alerts.quarantinedFilePath',
|
||||
{
|
||||
defaultMessage: 'Quarantined file path',
|
||||
}
|
||||
);
|
||||
|
||||
export const RULE_TYPE = i18n.translate('xpack.securitySolution.detections.alerts.ruleType', {
|
||||
defaultMessage: 'Rule type',
|
||||
});
|
||||
|
||||
export const ACTIONS = i18n.translate('xpack.securitySolution.eventDetails.table.actions', {
|
||||
defaultMessage: 'Actions',
|
||||
});
|
||||
|
||||
export const ALERT_REASON = i18n.translate('xpack.securitySolution.eventDetails.alertReason', {
|
||||
defaultMessage: 'Alert reason',
|
||||
});
|
||||
|
||||
export const ENDPOINT_COMMANDS = Object.freeze({
|
||||
tried: (command: string) =>
|
||||
i18n.translate('xpack.securitySolution.eventDetails.responseActions.endpoint.tried', {
|
||||
values: { command },
|
||||
defaultMessage: 'tried to execute {command} command',
|
||||
}),
|
||||
executed: (command: string) =>
|
||||
i18n.translate('xpack.securitySolution.eventDetails.responseActions.endpoint.executed', {
|
||||
values: { command },
|
||||
defaultMessage: 'executed {command} command',
|
||||
}),
|
||||
pending: (command: string) =>
|
||||
i18n.translate('xpack.securitySolution.eventDetails.responseActions.endpoint.pending', {
|
||||
values: { command },
|
||||
defaultMessage: 'is executing {command} command',
|
||||
}),
|
||||
failed: (command: string) =>
|
||||
i18n.translate('xpack.securitySolution.eventDetails.responseActions.endpoint.failed', {
|
||||
values: { command },
|
||||
defaultMessage: 'failed to execute {command} command',
|
||||
}),
|
||||
});
|
||||
|
||||
export const SUMMARY_VIEW = i18n.translate('xpack.securitySolution.eventDetails.summaryView', {
|
||||
defaultMessage: 'summary',
|
||||
});
|
||||
|
||||
export const ALERT_SUMMARY_CONVERSATION_ID = i18n.translate(
|
||||
'xpack.securitySolution.alertSummaryView.alertSummaryViewConversationId',
|
||||
{
|
||||
|
|
|
@ -17,17 +17,6 @@ export interface FieldsData {
|
|||
isObjectArray: boolean;
|
||||
}
|
||||
|
||||
export interface EnrichedFieldInfo {
|
||||
data: FieldsData | EventFieldsData;
|
||||
eventId: string;
|
||||
fieldFromBrowserField?: Partial<FieldSpec>;
|
||||
scopeId: string;
|
||||
values: string[] | null | undefined;
|
||||
linkValue?: string;
|
||||
}
|
||||
|
||||
export type EnrichedFieldInfoWithValues = EnrichedFieldInfo & { values: string[] };
|
||||
|
||||
export interface EventSummaryField {
|
||||
id: string;
|
||||
legacyId?: string;
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { escapeDataProviderId } from '@kbn/securitysolution-t-grid';
|
||||
|
||||
import { getDisplayValue } from '../../../timelines/components/timeline/data_providers/helpers';
|
||||
import type { DataProvider, DataProvidersAnd, QueryOperator } from '../../../../common/types';
|
||||
import { IS_OPERATOR } from '../../../../common/types';
|
||||
|
||||
export const getDataProvider = (
|
||||
field: string,
|
||||
id: string,
|
||||
value: string | string[],
|
||||
operator: QueryOperator = IS_OPERATOR,
|
||||
excluded: boolean = false
|
||||
): DataProvider => ({
|
||||
and: [],
|
||||
enabled: true,
|
||||
id: escapeDataProviderId(id),
|
||||
name: field,
|
||||
excluded,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field,
|
||||
value,
|
||||
operator,
|
||||
displayValue: getDisplayValue(value),
|
||||
},
|
||||
});
|
||||
|
||||
export const getDataProviderAnd = (
|
||||
field: string,
|
||||
id: string,
|
||||
value: string | string[],
|
||||
operator: QueryOperator = IS_OPERATOR,
|
||||
excluded: boolean = false
|
||||
): DataProvidersAnd => {
|
||||
const { and, ...dataProvider } = getDataProvider(field, id, value, operator, excluded);
|
||||
return dataProvider;
|
||||
};
|
|
@ -16,15 +16,15 @@ import {
|
|||
import { KibanaServices } from '../../../../lib/kibana';
|
||||
import type { DefaultTimeRangeSetting } from '../../../../utils/default_date_settings';
|
||||
import { plugin, renderer as Renderer } from '.';
|
||||
import type { InvestigateInTimelineButtonProps } from '../../../event_details/table/investigate_in_timeline_button';
|
||||
import type { InvestigateInTimelineButtonProps } from '../../../event_details/investigate_in_timeline_button';
|
||||
import { useUpsellingMessage } from '../../../../hooks/use_upselling';
|
||||
|
||||
jest.mock('../../../../lib/kibana');
|
||||
const mockGetServices = KibanaServices.get as jest.Mock;
|
||||
|
||||
jest.mock('../../../event_details/table/investigate_in_timeline_button', () => {
|
||||
jest.mock('../../../event_details/investigate_in_timeline_button', () => {
|
||||
const originalModule = jest.requireActual(
|
||||
'../../../event_details/table/investigate_in_timeline_button'
|
||||
'../../../event_details/investigate_in_timeline_button'
|
||||
);
|
||||
return {
|
||||
...originalModule,
|
||||
|
|
|
@ -43,7 +43,7 @@ import { useKibana } from '../../../../lib/kibana';
|
|||
import { useInsightQuery } from './use_insight_query';
|
||||
import { useInsightDataProviders, type Provider } from './use_insight_data_providers';
|
||||
import { BasicAlertDataContext } from '../../../../../flyout/document_details/left/components/investigation_guide_view';
|
||||
import { InvestigateInTimelineButton } from '../../../event_details/table/investigate_in_timeline_button';
|
||||
import { InvestigateInTimelineButton } from '../../../event_details/investigate_in_timeline_button';
|
||||
import {
|
||||
getTimeRangeSettings,
|
||||
parseDateWithDefault,
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
useInsightDataProviders,
|
||||
type UseInsightDataProvidersResult,
|
||||
} from './use_insight_data_providers';
|
||||
import { mockAlertDetailsData } from '../../../event_details/__mocks__';
|
||||
import { mockAlertDetailsData } from '../../../event_details/mocks';
|
||||
|
||||
const mockAlertDetailsDataWithIsObject = mockAlertDetailsData.map((detail) => {
|
||||
return {
|
||||
|
|
|
@ -15,7 +15,7 @@ import type {
|
|||
import { useUserPrivileges } from '../user_privileges';
|
||||
import { useGetAutomatedActionResponseList } from '../../../management/hooks/response_actions/use_get_automated_action_list';
|
||||
import { ActionsLogExpandedTray } from '../../../management/components/endpoint_response_actions_list/components/action_log_expanded_tray';
|
||||
import { ENDPOINT_COMMANDS } from '../event_details/translations';
|
||||
import { ENDPOINT_COMMANDS } from './translations';
|
||||
import { ResponseActionsEmptyPrompt } from './response_actions_empty_prompt';
|
||||
|
||||
interface EndpointResponseActionResultsProps {
|
||||
|
|
|
@ -13,3 +13,26 @@ export const LOAD_CONNECTORS_ERROR_MESSAGE = i18n.translate(
|
|||
defaultMessage: 'Error loading connectors. Please check your configuration and try again.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ENDPOINT_COMMANDS = Object.freeze({
|
||||
tried: (command: string) =>
|
||||
i18n.translate('xpack.securitySolution.eventDetails.responseActions.endpoint.tried', {
|
||||
values: { command },
|
||||
defaultMessage: 'tried to execute {command} command',
|
||||
}),
|
||||
executed: (command: string) =>
|
||||
i18n.translate('xpack.securitySolution.eventDetails.responseActions.endpoint.executed', {
|
||||
values: { command },
|
||||
defaultMessage: 'executed {command} command',
|
||||
}),
|
||||
pending: (command: string) =>
|
||||
i18n.translate('xpack.securitySolution.eventDetails.responseActions.endpoint.pending', {
|
||||
values: { command },
|
||||
defaultMessage: 'is executing {command} command',
|
||||
}),
|
||||
failed: (command: string) =>
|
||||
i18n.translate('xpack.securitySolution.eventDetails.responseActions.endpoint.failed', {
|
||||
values: { command },
|
||||
defaultMessage: 'failed to execute {command} command',
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -1,24 +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 type { Threats } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import type { SearchHit } from '../../../common/search_strategy';
|
||||
import { buildThreatDescription } from '../../detection_engine/rule_creation_ui/components/description_step/helpers';
|
||||
|
||||
// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462
|
||||
export const getMitreComponentParts = (searchHit?: SearchHit) => {
|
||||
const ruleParameters = searchHit?.fields
|
||||
? searchHit?.fields['kibana.alert.rule.parameters']
|
||||
: null;
|
||||
const threat = ruleParameters ? (ruleParameters[0]?.threat as Threats) : null;
|
||||
return threat && threat.length > 0
|
||||
? buildThreatDescription({
|
||||
label: threat[0].framework,
|
||||
threat,
|
||||
})
|
||||
: null;
|
||||
};
|
|
@ -155,16 +155,6 @@ jest.mock('../../../common/lib/kibana', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../timelines/components/side_panel/hooks/use_detail_panel', () => {
|
||||
return {
|
||||
useDetailPanel: () => ({
|
||||
openEventDetailsPanel: jest.fn(),
|
||||
handleOnDetailsPanelClosed: () => {},
|
||||
DetailsPanel: () => <div />,
|
||||
shouldShowDetailsPanel: false,
|
||||
}),
|
||||
};
|
||||
});
|
||||
const dataViewId = 'security-solution-default';
|
||||
|
||||
const stateWithBuildingBlockAlertsEnabled: State = {
|
||||
|
|
|
@ -20,9 +20,9 @@ import { CellTooltipWrapper } from '../../shared/components/cell_tooltip_wrapper
|
|||
import type { DataProvider } from '../../../../../common/types';
|
||||
import { SeverityBadge } from '../../../../common/components/severity_badge';
|
||||
import { usePaginatedAlerts } from '../hooks/use_paginated_alerts';
|
||||
import { InvestigateInTimelineButton } from '../../../../common/components/event_details/table/investigate_in_timeline_button';
|
||||
import { InvestigateInTimelineButton } from '../../../../common/components/event_details/investigate_in_timeline_button';
|
||||
import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations';
|
||||
import { getDataProvider } from '../../../../common/components/event_details/table/use_action_cell_data_provider';
|
||||
import { getDataProvider } from '../../../../common/components/event_details/use_action_cell_data_provider';
|
||||
import { AlertPreviewButton } from '../../../shared/components/alert_preview_button';
|
||||
|
||||
export const TIMESTAMP_DATE_FORMAT = 'MMM D, YYYY @ HH:mm:ss.SSS';
|
||||
|
|
|
@ -26,7 +26,7 @@ import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
|||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { FormattedCount } from '../../../../common/components/formatted_number';
|
||||
import { useLicense } from '../../../../common/hooks/use_license';
|
||||
import { InvestigateInTimelineButton } from '../../../../common/components/event_details/table/investigate_in_timeline_button';
|
||||
import { InvestigateInTimelineButton } from '../../../../common/components/event_details/investigate_in_timeline_button';
|
||||
import type { PrevalenceData } from '../../shared/hooks/use_prevalence';
|
||||
import { usePrevalence } from '../../shared/hooks/use_prevalence';
|
||||
import {
|
||||
|
@ -47,7 +47,7 @@ import { useDocumentDetailsContext } from '../../shared/context';
|
|||
import {
|
||||
getDataProvider,
|
||||
getDataProviderAnd,
|
||||
} from '../../../../common/components/event_details/table/use_action_cell_data_provider';
|
||||
} from '../../../../common/components/event_details/use_action_cell_data_provider';
|
||||
import { getEmptyTagValue } from '../../../../common/components/empty_value';
|
||||
import { IS_OPERATOR } from '../../../../../common/types';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
|
|
@ -11,15 +11,22 @@ import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
|||
import { ALERT_RULE_TYPE } from '@kbn/rule-data-utils';
|
||||
import { EuiBetaBadge, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ExpandablePanel } from '@kbn/security-solution-common';
|
||||
import {
|
||||
CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID,
|
||||
SUPPRESSED_ALERTS_SECTION_TECHNICAL_PREVIEW_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { SUPPRESSED_ALERTS_COUNT_TECHNICAL_PREVIEW } from '../../../../common/components/event_details/insights/translations';
|
||||
import { InvestigateInTimelineAction } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action';
|
||||
import { isSuppressionRuleInGA } from '../../../../../common/detection_engine/utils';
|
||||
|
||||
const SUPPRESSED_ALERTS_COUNT_TECHNICAL_PREVIEW = i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.suppressedAlertsCountTechnicalPreview',
|
||||
{
|
||||
defaultMessage: 'Technical Preview',
|
||||
}
|
||||
);
|
||||
|
||||
export interface SuppressedAlertsProps {
|
||||
/**
|
||||
* An object with top level fields from the ECS object
|
||||
|
|
|
@ -8,9 +8,27 @@
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import type { FC } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { MITRE_ATTACK_DETAILS_TEST_ID, MITRE_ATTACK_TITLE_TEST_ID } from './test_ids';
|
||||
import { getMitreComponentParts } from '../../../../detections/mitre/get_mitre_threat_component';
|
||||
import type { Threats } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import type { SearchHit } from '../../../../../common/search_strategy';
|
||||
import { buildThreatDescription } from '../../../../detection_engine/rule_creation_ui/components/description_step/helpers';
|
||||
import { useDocumentDetailsContext } from '../../shared/context';
|
||||
import { MITRE_ATTACK_DETAILS_TEST_ID, MITRE_ATTACK_TITLE_TEST_ID } from './test_ids';
|
||||
|
||||
/**
|
||||
* Retrieves mitre attack information from the alert information
|
||||
*/
|
||||
const getMitreComponentParts = (searchHit?: SearchHit) => {
|
||||
const ruleParameters = searchHit?.fields
|
||||
? searchHit?.fields['kibana.alert.rule.parameters']
|
||||
: null;
|
||||
const threat = ruleParameters ? (ruleParameters[0]?.threat as Threats) : null;
|
||||
return threat && threat.length > 0
|
||||
? buildThreatDescription({
|
||||
label: threat[0].framework,
|
||||
threat,
|
||||
})
|
||||
: null;
|
||||
};
|
||||
|
||||
export const MitreAttack: FC = () => {
|
||||
const { searchHit } = useDocumentDetailsContext();
|
||||
|
|
|
@ -9,17 +9,13 @@ import type { FC } from 'react';
|
|||
import React, { useMemo } from 'react';
|
||||
import { find } from 'lodash/fp';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
|
||||
import { getEmptyTagValue } from '../../../../common/components/empty_value';
|
||||
import type {
|
||||
EnrichedFieldInfo,
|
||||
EnrichedFieldInfoWithValues,
|
||||
} from '../../../../common/components/event_details/types';
|
||||
import { SIGNAL_STATUS_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { StatusPopoverButton } from '../../../../common/components/event_details/overview/status_popover_button';
|
||||
import { StatusPopoverButton } from './status_popover_button';
|
||||
import { useDocumentDetailsContext } from '../../shared/context';
|
||||
import { getEnrichedFieldInfo } from '../../../../common/components/event_details/helpers';
|
||||
import type { EnrichedFieldInfo, EnrichedFieldInfoWithValues } from '../utils/enriched_field_info';
|
||||
import { getEnrichedFieldInfo } from '../utils/enriched_field_info';
|
||||
import { CellActions } from './cell_actions';
|
||||
import { STATUS_TITLE_TEST_ID } from './test_ids';
|
||||
|
||||
|
@ -34,7 +30,6 @@ function hasData(fieldInfo?: EnrichedFieldInfo): fieldInfo is EnrichedFieldInfoW
|
|||
* Document details status displayed in flyout right section header
|
||||
*/
|
||||
export const DocumentStatus: FC = () => {
|
||||
const { closeFlyout } = useExpandableFlyoutApi();
|
||||
const { eventId, browserFields, dataFormattedForFieldBrowser, scopeId, isPreview } =
|
||||
useDocumentDetailsContext();
|
||||
|
||||
|
@ -77,7 +72,6 @@ export const DocumentStatus: FC = () => {
|
|||
contextId={scopeId}
|
||||
enrichedFieldInfo={statusData}
|
||||
scopeId={scopeId}
|
||||
handleOnEventClosed={closeFlyout}
|
||||
/>
|
||||
</CellActions>
|
||||
)}
|
||||
|
|
|
@ -9,8 +9,9 @@ import React from 'react';
|
|||
import { render } from '@testing-library/react';
|
||||
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
|
||||
import { StatusPopoverButton } from './status_popover_button';
|
||||
import { TestProviders } from '../../../mock';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { useAlertsPrivileges } from '../../../../detections/containers/detection_engine/alerts/use_alerts_privileges';
|
||||
|
||||
const props = {
|
||||
eventId: 'testid',
|
||||
contextId: 'alerts-page',
|
|
@ -6,8 +6,10 @@
|
|||
*/
|
||||
|
||||
import { EuiContextMenu, EuiPopover, EuiPopoverTitle } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { getFieldFormat } from '../utils/get_field_format';
|
||||
import type { EnrichedFieldInfoWithValues } from '../utils/enriched_field_info';
|
||||
import { useAlertsActions } from '../../../../detections/components/alerts_table/timeline_actions/use_alerts_actions';
|
||||
import type { Status } from '../../../../../common/api/detection_engine';
|
||||
import {
|
||||
|
@ -15,30 +17,44 @@ import {
|
|||
CLICK_TO_CHANGE_ALERT_STATUS,
|
||||
} from '../../../../detections/components/alerts_table/translations';
|
||||
import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field';
|
||||
import type { EnrichedFieldInfoWithValues } from '../types';
|
||||
import type { inputsModel } from '../../../store';
|
||||
import { inputsSelectors } from '../../../store';
|
||||
import { useDeepEqualSelector } from '../../../hooks/use_selector';
|
||||
import { getFieldFormat } from '../get_field_format';
|
||||
import type { inputsModel } from '../../../../common/store';
|
||||
import { inputsSelectors } from '../../../../common/store';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
|
||||
interface StatusPopoverButtonProps {
|
||||
/**
|
||||
* Id of the document
|
||||
*/
|
||||
eventId: string;
|
||||
/**
|
||||
* Value used to create a unique identifier in children components
|
||||
*/
|
||||
contextId: string;
|
||||
/**
|
||||
* Information used to
|
||||
*/
|
||||
enrichedFieldInfo: EnrichedFieldInfoWithValues;
|
||||
/**
|
||||
* Maintain backwards compatibility // TODO remove when possible
|
||||
*/
|
||||
scopeId: string;
|
||||
handleOnEventClosed: () => void;
|
||||
}
|
||||
|
||||
// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462
|
||||
export const StatusPopoverButton = React.memo<StatusPopoverButtonProps>(
|
||||
({ eventId, contextId, enrichedFieldInfo, scopeId, handleOnEventClosed }) => {
|
||||
/**
|
||||
* Renders a button and its popover to display the status of an alert and allows the user to change it.
|
||||
* It is used in the header of the document details flyout.
|
||||
*/
|
||||
export const StatusPopoverButton = memo(
|
||||
({ eventId, contextId, enrichedFieldInfo, scopeId }: StatusPopoverButtonProps) => {
|
||||
const { closeFlyout } = useExpandableFlyoutApi();
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]);
|
||||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
const closeAfterAction = useCallback(() => {
|
||||
closePopover();
|
||||
handleOnEventClosed();
|
||||
}, [closePopover, handleOnEventClosed]);
|
||||
closeFlyout();
|
||||
}, [closeFlyout, closePopover]);
|
||||
|
||||
const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuery(), []);
|
||||
|
|
@ -9,6 +9,7 @@ import React from 'react';
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiBetaBadge } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isSuppressionRuleInGA } from '../../../../../common/detection_engine/utils';
|
||||
|
||||
import {
|
||||
|
@ -16,7 +17,13 @@ import {
|
|||
CORRELATIONS_SUPPRESSED_ALERTS_TECHNICAL_PREVIEW_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { InsightsSummaryRow } from './insights_summary_row';
|
||||
import { SUPPRESSED_ALERTS_COUNT_TECHNICAL_PREVIEW } from '../../../../common/components/event_details/insights/translations';
|
||||
|
||||
const SUPPRESSED_ALERTS_COUNT_TECHNICAL_PREVIEW = i18n.translate(
|
||||
'xpack.securitySolution.flyout.right.overview.insights.suppressedAlertsCountTechnicalPreview',
|
||||
{
|
||||
defaultMessage: 'Technical Preview',
|
||||
}
|
||||
);
|
||||
|
||||
export interface SuppressedAlertsProps {
|
||||
/**
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React, { memo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import type { FieldSpec } from '@kbn/data-plugin/common';
|
||||
import { getFieldFormat } from '../../../../common/components/event_details/get_field_format';
|
||||
import { getFieldFormat } from '../utils/get_field_format';
|
||||
import type { EventFieldsData } from '../../../../common/components/event_details/types';
|
||||
import { OverflowField } from '../../../../common/components/tables/helpers';
|
||||
import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field';
|
||||
|
|
|
@ -11,7 +11,7 @@ import { waitFor } from '@testing-library/react';
|
|||
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
import type { TakeActionDropdownProps } from './take_action_dropdown';
|
||||
import { TakeActionDropdown } from './take_action_dropdown';
|
||||
import { generateAlertDetailsDataMock } from '../../../../common/components/event_details/__mocks__';
|
||||
import { mockAlertDetailsData } from '../../../../common/components/event_details/mocks';
|
||||
import { getDetectionAlertMock } from '../../../../common/mock/mock_detection_alerts';
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
|
@ -77,7 +77,7 @@ describe('take action dropdown', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
defaultProps = {
|
||||
dataFormattedForFieldBrowser: generateAlertDetailsDataMock() as TimelineEventsDetailsItem[],
|
||||
dataFormattedForFieldBrowser: mockAlertDetailsData as TimelineEventsDetailsItem[],
|
||||
dataAsNestedObject: getDetectionAlertMock(),
|
||||
handleOnEventClosed: jest.fn(),
|
||||
isHostIsolationPanelOpen: false,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
import { useAssistantOverlay } from '@kbn/elastic-assistant';
|
||||
import { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability';
|
||||
import { getRawData } from '../../../../assistant/helpers';
|
||||
import {
|
||||
|
@ -17,7 +18,6 @@ import {
|
|||
EVENT_SUMMARY_CONTEXT_DESCRIPTION,
|
||||
EVENT_SUMMARY_CONVERSATION_ID,
|
||||
EVENT_SUMMARY_VIEW_CONTEXT_TOOLTIP,
|
||||
SUMMARY_VIEW,
|
||||
} from '../../../../common/components/event_details/translations';
|
||||
import {
|
||||
PROMPT_CONTEXT_ALERT_CATEGORY,
|
||||
|
@ -25,6 +25,10 @@ import {
|
|||
PROMPT_CONTEXTS,
|
||||
} from '../../../../assistant/content/prompt_contexts';
|
||||
|
||||
const SUMMARY_VIEW = i18n.translate('xpack.securitySolution.eventDetails.summaryView', {
|
||||
defaultMessage: 'summary',
|
||||
});
|
||||
|
||||
const useAssistantNoop = () => ({ promptContextId: undefined });
|
||||
|
||||
export interface UseAssistantParams {
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
import { get, getOr } from 'lodash/fp';
|
||||
import type { FieldSpec } from '@kbn/data-views-plugin/common';
|
||||
import type {
|
||||
EventFieldsData,
|
||||
EventSummaryField,
|
||||
FieldsData,
|
||||
} from '../../../../common/components/event_details/types';
|
||||
|
||||
export interface EnrichedFieldInfo {
|
||||
data: FieldsData | EventFieldsData;
|
||||
eventId: string;
|
||||
fieldFromBrowserField?: Partial<FieldSpec>;
|
||||
scopeId: string;
|
||||
values: string[] | null | undefined;
|
||||
linkValue?: string;
|
||||
}
|
||||
|
||||
export type EnrichedFieldInfoWithValues = EnrichedFieldInfo & { values: string[] };
|
||||
|
||||
export function getEnrichedFieldInfo({
|
||||
browserFields,
|
||||
contextId,
|
||||
eventId,
|
||||
field,
|
||||
item,
|
||||
linkValueField,
|
||||
scopeId,
|
||||
}: {
|
||||
browserFields: BrowserFields;
|
||||
contextId: string;
|
||||
item: TimelineEventsDetailsItem;
|
||||
eventId: string;
|
||||
field?: EventSummaryField;
|
||||
scopeId: string;
|
||||
linkValueField?: TimelineEventsDetailsItem;
|
||||
}): EnrichedFieldInfo {
|
||||
const fieldInfo = {
|
||||
contextId,
|
||||
eventId,
|
||||
fieldType: 'string',
|
||||
linkValue: undefined,
|
||||
scopeId,
|
||||
};
|
||||
const linkValue = getOr(null, 'originalValue.0', linkValueField);
|
||||
const category = item.category ?? '';
|
||||
const fieldName = item.field ?? '';
|
||||
|
||||
const browserField = get([category, 'fields', fieldName], browserFields);
|
||||
const overrideField = field?.overrideField;
|
||||
return {
|
||||
...fieldInfo,
|
||||
data: {
|
||||
field: overrideField ?? fieldName,
|
||||
format: browserField?.format?.id ?? '',
|
||||
type: browserField?.type ?? '',
|
||||
isObjectArray: item.isObjectArray,
|
||||
},
|
||||
values: item.values,
|
||||
linkValue: linkValue ?? undefined,
|
||||
fieldFromBrowserField: browserField,
|
||||
};
|
||||
}
|
|
@ -13,7 +13,7 @@ import { sourcererActions } from '../../../../sourcerer/store';
|
|||
import {
|
||||
getDataProvider,
|
||||
getDataProviderAnd,
|
||||
} from '../../../../common/components/event_details/table/use_action_cell_data_provider';
|
||||
} from '../../../../common/components/event_details/use_action_cell_data_provider';
|
||||
import type { DataProvider, QueryOperator } from '../../../../../common/types/timeline';
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
import { TimelineTypeEnum } from '../../../../../common/api/timeline';
|
||||
|
|
|
@ -1,43 +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 { executeAction } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useKibana } from '../../../../../common/lib/kibana/kibana_react';
|
||||
|
||||
export interface UseSubActionParams<P, R> {
|
||||
connectorId?: string;
|
||||
subAction: string;
|
||||
subActionParams?: P;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const useSubAction = <P, R>({
|
||||
connectorId,
|
||||
subAction,
|
||||
subActionParams,
|
||||
disabled = false,
|
||||
...rest
|
||||
}: UseSubActionParams<P, R>) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['useSubAction', connectorId, subAction, subActionParams],
|
||||
queryFn: ({ signal }) =>
|
||||
executeAction<R>({
|
||||
id: connectorId as string,
|
||||
params: {
|
||||
subAction,
|
||||
subActionParams,
|
||||
},
|
||||
http,
|
||||
signal,
|
||||
}),
|
||||
enabled: !disabled && !!connectorId && !!subAction,
|
||||
...rest,
|
||||
});
|
||||
};
|
|
@ -1,37 +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 { executeAction } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useKibana } from '../../../../../common/lib/kibana/kibana_react';
|
||||
|
||||
export interface UseSubActionParams<P> {
|
||||
connectorId: string;
|
||||
subAction: string;
|
||||
subActionParams?: P;
|
||||
}
|
||||
|
||||
export const useSubActionMutation = <P, R>({
|
||||
connectorId,
|
||||
subAction,
|
||||
subActionParams,
|
||||
}: UseSubActionParams<P>) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['executeSubAction', connectorId, subAction, subActionParams],
|
||||
mutationFn: () =>
|
||||
executeAction<R>({
|
||||
id: connectorId,
|
||||
params: {
|
||||
subAction,
|
||||
subActionParams,
|
||||
},
|
||||
http,
|
||||
}),
|
||||
});
|
||||
};
|
|
@ -1,25 +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 { DEFAULT_ALERTS_INDEX, DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants';
|
||||
|
||||
/*
|
||||
The referenced alert _index in the flyout uses the `.internal.` such as
|
||||
`.internal.alerts-security.alerts-spaceId` in the alert page flyout and
|
||||
.internal.preview.alerts-security.alerts-spaceId` in the rule creation preview flyout
|
||||
but we always want to use their respective aliase indices rather than accessing their backing .internal. indices.
|
||||
*/
|
||||
export const getAlertIndexAlias = (
|
||||
index: string,
|
||||
spaceId: string = 'default'
|
||||
): string | undefined => {
|
||||
if (index.startsWith(`.internal${DEFAULT_ALERTS_INDEX}`)) {
|
||||
return `${DEFAULT_ALERTS_INDEX}-${spaceId}`;
|
||||
} else if (index.startsWith(`.internal${DEFAULT_PREVIEW_INDEX}`)) {
|
||||
return `${DEFAULT_PREVIEW_INDEX}-${spaceId}`;
|
||||
}
|
||||
};
|
|
@ -1,79 +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 { renderHook, act } from '@testing-library/react-hooks';
|
||||
import React from 'react';
|
||||
import type { UseDetailPanelConfig } from './use_detail_panel';
|
||||
import { useDetailPanel } from './use_detail_panel';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { SourcererScopeName } from '../../../../sourcerer/store/model';
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
import { ExpandableFlyoutProvider } from '@kbn/expandable-flyout';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock';
|
||||
|
||||
const mockedTelemetry = createTelemetryServiceMock();
|
||||
jest.mock('../../../../common/lib/kibana', () => {
|
||||
const original = jest.requireActual('../../../../common/lib/kibana');
|
||||
return {
|
||||
...original,
|
||||
useKibana: () => ({
|
||||
...original.useKibana(),
|
||||
services: {
|
||||
...original.useKibana().services,
|
||||
telemetry: mockedTelemetry,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
jest.mock('../../../../common/hooks/use_selector');
|
||||
jest.mock('../../../store');
|
||||
|
||||
jest.mock('../../../../sourcerer/containers', () => {
|
||||
const mockSourcererReturn = {
|
||||
browserFields: {},
|
||||
loading: true,
|
||||
indexPattern: {},
|
||||
selectedPatterns: [],
|
||||
missingPatterns: [],
|
||||
};
|
||||
return {
|
||||
useSourcererDataView: jest.fn().mockReturnValue(mockSourcererReturn),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useDetailPanel', () => {
|
||||
const defaultProps: UseDetailPanelConfig = {
|
||||
sourcererScope: SourcererScopeName.detections,
|
||||
scopeId: TimelineId.test,
|
||||
};
|
||||
const mockGetExpandedDetail = jest.fn().mockImplementation(() => ({}));
|
||||
beforeEach(() => {
|
||||
(useDeepEqualSelector as jest.Mock).mockImplementation((cb) => {
|
||||
return mockGetExpandedDetail();
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
(useDeepEqualSelector as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactChild }) => (
|
||||
<TestProviders>
|
||||
<ExpandableFlyoutProvider>{children}</ExpandableFlyoutProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
const renderUseDetailPanel = (props = defaultProps) =>
|
||||
renderHook(() => useDetailPanel(props), { wrapper });
|
||||
|
||||
test('should return open fns (event, host, network, user), handleOnDetailsPanelClosed fn, shouldShowDetailsPanel, and the DetailsPanel component', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderUseDetailPanel();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.openEventDetailsPanel).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,57 +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 { useMemo, useCallback } from 'react';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { useSourcererDataView } from '../../../../sourcerer/containers';
|
||||
import type { SourcererScopeName } from '../../../../sourcerer/store/model';
|
||||
import { DocumentDetailsRightPanelKey } from '../../../../flyout/document_details/shared/constants/panel_keys';
|
||||
|
||||
export interface UseDetailPanelConfig {
|
||||
sourcererScope: SourcererScopeName;
|
||||
scopeId: string;
|
||||
}
|
||||
export interface UseDetailPanelReturn {
|
||||
openEventDetailsPanel: (eventId?: string, onClose?: () => void) => void;
|
||||
}
|
||||
|
||||
export const useDetailPanel = ({
|
||||
sourcererScope,
|
||||
scopeId,
|
||||
}: UseDetailPanelConfig): UseDetailPanelReturn => {
|
||||
const { telemetry } = useKibana().services;
|
||||
const { selectedPatterns } = useSourcererDataView(sourcererScope);
|
||||
|
||||
const { openFlyout } = useExpandableFlyoutApi();
|
||||
|
||||
const eventDetailsIndex = useMemo(() => selectedPatterns.join(','), [selectedPatterns]);
|
||||
|
||||
const openEventDetailsPanel = useCallback(
|
||||
(eventId?: string, onClose?: () => void) => {
|
||||
openFlyout({
|
||||
right: {
|
||||
id: DocumentDetailsRightPanelKey,
|
||||
params: {
|
||||
id: eventId,
|
||||
indexName: eventDetailsIndex,
|
||||
scopeId,
|
||||
},
|
||||
},
|
||||
});
|
||||
telemetry.reportDetailsFlyoutOpened({
|
||||
location: scopeId,
|
||||
panel: 'right',
|
||||
});
|
||||
},
|
||||
[openFlyout, eventDetailsIndex, scopeId, telemetry]
|
||||
);
|
||||
|
||||
return {
|
||||
openEventDetailsPanel,
|
||||
};
|
||||
};
|
|
@ -68,18 +68,6 @@ jest.mock('../../../../../common/lib/kibana', () => {
|
|||
}),
|
||||
};
|
||||
});
|
||||
const mockOpenDetailFn = jest.fn();
|
||||
|
||||
jest.mock('../../../side_panel/hooks/use_detail_panel', () => {
|
||||
return {
|
||||
useDetailPanel: () => ({
|
||||
openEventDetailsPanel: mockOpenDetailFn,
|
||||
handleOnDetailsPanelClosed: () => {},
|
||||
DetailsPanel: () => <div />,
|
||||
shouldShowDetailsPanel: false,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useSessionView with active timeline and a session id and graph event id', () => {
|
||||
let setTimelineFullScreen: jest.Mock;
|
||||
|
@ -155,12 +143,7 @@ describe('useSessionView with active timeline and a session id and graph event i
|
|||
},
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
expect(kibana.services.sessionView.getSessionView).toHaveBeenCalledWith({
|
||||
height: 1000,
|
||||
sessionEntityId: 'test',
|
||||
loadAlertDetails: mockOpenDetailFn,
|
||||
canReadPolicyManagement: false,
|
||||
});
|
||||
expect(kibana.services.sessionView.getSessionView).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('useSessionView with non active timeline and graph event id set', () => {
|
||||
|
|
|
@ -11,6 +11,9 @@ import styled from 'styled-components';
|
|||
import { useDispatch } from 'react-redux';
|
||||
import { dataTableSelectors, tableDefaults } from '@kbn/securitysolution-data-table';
|
||||
import type { TableId } from '@kbn/securitysolution-data-table';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { DocumentDetailsRightPanelKey } from '../../../../../flyout/document_details/shared/constants/panel_keys';
|
||||
import { useSourcererDataView } from '../../../../../sourcerer/containers';
|
||||
import {
|
||||
getScopedActions,
|
||||
isActiveTimeline,
|
||||
|
@ -20,7 +23,6 @@ import {
|
|||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import * as i18n from './translations';
|
||||
import { TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { useDetailPanel } from '../../../side_panel/hooks/use_detail_panel';
|
||||
import { SourcererScopeName } from '../../../../../sourcerer/store/model';
|
||||
import { isFullScreen } from '../../body/column_headers';
|
||||
import { SCROLLING_DISABLED_CLASS_NAME } from '../../../../../../common/constants';
|
||||
|
@ -242,7 +244,7 @@ export const useSessionViewNavigation = ({ scopeId }: { scopeId: string }) => {
|
|||
};
|
||||
|
||||
export const useSessionView = ({ scopeId, height }: { scopeId: string; height?: number }) => {
|
||||
const { sessionView } = useKibana().services;
|
||||
const { sessionView, telemetry } = useKibana().services;
|
||||
const getScope = useMemo(() => {
|
||||
if (isTimelineScope(scopeId)) {
|
||||
return timelineSelectors.getTimelineByIdSelector();
|
||||
|
@ -280,10 +282,30 @@ export const useSessionView = ({ scopeId, height }: { scopeId: string; height?:
|
|||
return SourcererScopeName.default;
|
||||
}
|
||||
}, [scopeId]);
|
||||
const { openEventDetailsPanel } = useDetailPanel({
|
||||
sourcererScope,
|
||||
scopeId,
|
||||
});
|
||||
|
||||
const { selectedPatterns } = useSourcererDataView(sourcererScope);
|
||||
const eventDetailsIndex = useMemo(() => selectedPatterns.join(','), [selectedPatterns]);
|
||||
|
||||
const { openFlyout } = useExpandableFlyoutApi();
|
||||
const openAlertDetailsFlyout = useCallback(
|
||||
(eventId?: string, onClose?: () => void) => {
|
||||
openFlyout({
|
||||
right: {
|
||||
id: DocumentDetailsRightPanelKey,
|
||||
params: {
|
||||
id: eventId,
|
||||
indexName: eventDetailsIndex,
|
||||
scopeId,
|
||||
},
|
||||
},
|
||||
});
|
||||
telemetry.reportDetailsFlyoutOpened({
|
||||
location: scopeId,
|
||||
panel: 'right',
|
||||
});
|
||||
},
|
||||
[openFlyout, eventDetailsIndex, scopeId, telemetry]
|
||||
);
|
||||
|
||||
const sessionViewComponent = useMemo(() => {
|
||||
const sessionViewSearchBarHeight = 118;
|
||||
|
@ -291,7 +313,7 @@ export const useSessionView = ({ scopeId, height }: { scopeId: string; height?:
|
|||
return sessionViewConfig !== null
|
||||
? sessionView.getSessionView({
|
||||
...sessionViewConfig,
|
||||
loadAlertDetails: openEventDetailsPanel,
|
||||
loadAlertDetails: openAlertDetailsFlyout,
|
||||
isFullScreen: fullScreen,
|
||||
height: heightMinusSearchBar,
|
||||
canReadPolicyManagement,
|
||||
|
@ -301,13 +323,13 @@ export const useSessionView = ({ scopeId, height }: { scopeId: string; height?:
|
|||
height,
|
||||
sessionViewConfig,
|
||||
sessionView,
|
||||
openEventDetailsPanel,
|
||||
openAlertDetailsFlyout,
|
||||
fullScreen,
|
||||
canReadPolicyManagement,
|
||||
]);
|
||||
|
||||
return {
|
||||
openEventDetailsPanel,
|
||||
openEventDetailsPanel: openAlertDetailsFlyout,
|
||||
SessionView: sessionViewComponent,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -638,13 +638,6 @@ export const onKeyDownFocusHandler = ({
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* An `onFocus` event handler that focuses the first child draggable
|
||||
* keyboard handler
|
||||
*/
|
||||
export const onFocusReFocusDraggable = (event: React.FocusEvent<HTMLElement>) =>
|
||||
event.target.querySelector<HTMLDivElement>(`.${DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME}`)?.focus();
|
||||
|
||||
/** Returns `true` when the element, or one of it's children has focus */
|
||||
export const elementOrChildrenHasFocus = (element: HTMLElement | null | undefined): boolean =>
|
||||
element === document.activeElement || element?.querySelector(':focus-within') != null;
|
||||
|
@ -661,18 +654,6 @@ export type FocusableElement =
|
|||
| HTMLTextAreaElement
|
||||
| HTMLVideoElement;
|
||||
|
||||
/**
|
||||
* This function has a side effect. It focuses the first element with a
|
||||
* matching `className` in the `containerElement`.
|
||||
*/
|
||||
export const skipFocusInContainerTo = ({
|
||||
containerElement,
|
||||
className,
|
||||
}: {
|
||||
containerElement: HTMLElement | null;
|
||||
className: string;
|
||||
}) => containerElement?.querySelector<FocusableElement>(`.${className}`)?.focus();
|
||||
|
||||
/**
|
||||
* Returns a table cell's focusable children, which may be one of the following
|
||||
* a) a `HTMLButtonElement` that does NOT have the `disabled` attribute
|
||||
|
|
|
@ -35423,7 +35423,6 @@
|
|||
"xpack.securitySolution.alertCountByRuleByStatus.status": "Statut",
|
||||
"xpack.securitySolution.alertCountByRuleByStatus.tooltipTitle": "Nom de règle",
|
||||
"xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "Données de risque de {riskEntity}",
|
||||
"xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCountTechnicalPreview": "Version d'évaluation technique",
|
||||
"xpack.securitySolution.alertDetails.summary.readLess": "Lire moins",
|
||||
"xpack.securitySolution.alertDetails.summary.readMore": "En savoir plus",
|
||||
"xpack.securitySolution.alerts.badge.readOnly.tooltip": "Impossible de mettre à jour les alertes",
|
||||
|
@ -38786,15 +38785,11 @@
|
|||
"xpack.securitySolution.event.summary.threat_indicator.modal.allMatches": "Toutes les correspondances d'indicateur",
|
||||
"xpack.securitySolution.event.summary.threat_indicator.modal.close": "Fermer",
|
||||
"xpack.securitySolution.event.summary.threat_indicator.showMatches": "Afficher les {count} alertes de correspondance d'indicateur",
|
||||
"xpack.securitySolution.eventDetails.alertReason": "Raison d'alerte",
|
||||
"xpack.securitySolution.eventDetails.description": "Description",
|
||||
"xpack.securitySolution.eventDetails.responseActions.endpoint.executed": "a exécuté la commande {command}",
|
||||
"xpack.securitySolution.eventDetails.responseActions.endpoint.failed": "n'a pas pu exécuter la commande {command}",
|
||||
"xpack.securitySolution.eventDetails.responseActions.endpoint.pending": "exécute la commande {command}",
|
||||
"xpack.securitySolution.eventDetails.responseActions.endpoint.tried": "a tenté d'exécuter la commande {command}",
|
||||
"xpack.securitySolution.eventDetails.summaryView": "résumé",
|
||||
"xpack.securitySolution.eventDetails.table": "Tableau",
|
||||
"xpack.securitySolution.eventDetails.table.actions": "Actions",
|
||||
"xpack.securitySolution.eventFilter.flyoutForm.confirmModal.name": "filtre d'événements",
|
||||
"xpack.securitySolution.eventFilter.flyoutForm.creationSuccessToastTitle": "\"{name}\" a été ajouté à la liste de filtres d'événements.",
|
||||
"xpack.securitySolution.eventFilter.form.description.placeholder": "Description",
|
||||
|
|
|
@ -35408,7 +35408,6 @@
|
|||
"xpack.securitySolution.alertCountByRuleByStatus.status": "ステータス",
|
||||
"xpack.securitySolution.alertCountByRuleByStatus.tooltipTitle": "ルール名",
|
||||
"xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "{riskEntity}リスクデータ",
|
||||
"xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCountTechnicalPreview": "テクニカルプレビュー",
|
||||
"xpack.securitySolution.alertDetails.summary.readLess": "表示を減らす",
|
||||
"xpack.securitySolution.alertDetails.summary.readMore": "続きを読む",
|
||||
"xpack.securitySolution.alerts.badge.readOnly.tooltip": "アラートを更新できません",
|
||||
|
@ -38768,15 +38767,11 @@
|
|||
"xpack.securitySolution.event.summary.threat_indicator.modal.allMatches": "すべてのインジケーター一致",
|
||||
"xpack.securitySolution.event.summary.threat_indicator.modal.close": "閉じる",
|
||||
"xpack.securitySolution.event.summary.threat_indicator.showMatches": "すべての{count}件のインジケーター一致アラートを表示",
|
||||
"xpack.securitySolution.eventDetails.alertReason": "アラートの理由",
|
||||
"xpack.securitySolution.eventDetails.description": "説明",
|
||||
"xpack.securitySolution.eventDetails.responseActions.endpoint.executed": "{command}コマンドを実行しました",
|
||||
"xpack.securitySolution.eventDetails.responseActions.endpoint.failed": "{command}コマンドを実行できませんでした",
|
||||
"xpack.securitySolution.eventDetails.responseActions.endpoint.pending": "{command}コマンドを実行しています",
|
||||
"xpack.securitySolution.eventDetails.responseActions.endpoint.tried": "{command}コマンドを実行しようとしました",
|
||||
"xpack.securitySolution.eventDetails.summaryView": "まとめ",
|
||||
"xpack.securitySolution.eventDetails.table": "表",
|
||||
"xpack.securitySolution.eventDetails.table.actions": "アクション",
|
||||
"xpack.securitySolution.eventFilter.flyoutForm.confirmModal.name": "イベントフィルター",
|
||||
"xpack.securitySolution.eventFilter.flyoutForm.creationSuccessToastTitle": "\"{name}\"がイベントフィルターリストに追加されました。",
|
||||
"xpack.securitySolution.eventFilter.form.description.placeholder": "説明",
|
||||
|
|
|
@ -35449,7 +35449,6 @@
|
|||
"xpack.securitySolution.alertCountByRuleByStatus.status": "状态",
|
||||
"xpack.securitySolution.alertCountByRuleByStatus.tooltipTitle": "规则名称",
|
||||
"xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "{riskEntity}风险数据",
|
||||
"xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCountTechnicalPreview": "技术预览",
|
||||
"xpack.securitySolution.alertDetails.summary.readLess": "阅读更少内容",
|
||||
"xpack.securitySolution.alertDetails.summary.readMore": "阅读更多内容",
|
||||
"xpack.securitySolution.alerts.badge.readOnly.tooltip": "无法更新告警",
|
||||
|
@ -38812,15 +38811,11 @@
|
|||
"xpack.securitySolution.event.summary.threat_indicator.modal.allMatches": "所有指标匹配",
|
||||
"xpack.securitySolution.event.summary.threat_indicator.modal.close": "关闭",
|
||||
"xpack.securitySolution.event.summary.threat_indicator.showMatches": "显示所有 {count} 个指标匹配告警",
|
||||
"xpack.securitySolution.eventDetails.alertReason": "告警原因",
|
||||
"xpack.securitySolution.eventDetails.description": "描述",
|
||||
"xpack.securitySolution.eventDetails.responseActions.endpoint.executed": "已执行 {command} 命令",
|
||||
"xpack.securitySolution.eventDetails.responseActions.endpoint.failed": "无法执行 {command} 命令",
|
||||
"xpack.securitySolution.eventDetails.responseActions.endpoint.pending": "正在执行 {command} 命令",
|
||||
"xpack.securitySolution.eventDetails.responseActions.endpoint.tried": "已尝试执行 {command} 命令",
|
||||
"xpack.securitySolution.eventDetails.summaryView": "摘要",
|
||||
"xpack.securitySolution.eventDetails.table": "表",
|
||||
"xpack.securitySolution.eventDetails.table.actions": "操作",
|
||||
"xpack.securitySolution.eventFilter.flyoutForm.confirmModal.name": "事件筛选",
|
||||
"xpack.securitySolution.eventFilter.flyoutForm.creationSuccessToastTitle": "“{name}”已添加到事件筛选列表。",
|
||||
"xpack.securitySolution.eventFilter.form.description.placeholder": "描述",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue