[Security Solution][Alert details] - finish cleanup of old event_details folder (#190119)

This commit is contained in:
Philippe Oberti 2024-08-28 17:47:57 +02:00 committed by GitHub
parent 6b6bb3cfe0
commit 0cc275a5df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 295 additions and 1128 deletions

View file

@ -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';

View file

@ -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',
}

View file

@ -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'],
}),
}),
])
);
});
});

View file

@ -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]}`],
},
};
}
}

View file

@ -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];
}

View file

@ -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',
}
);

View file

@ -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', () => {

View file

@ -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;

View file

@ -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: '-' },

View file

@ -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;
};

View file

@ -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',
{

View file

@ -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;

View file

@ -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;
};

View file

@ -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,

View file

@ -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,

View file

@ -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 {

View file

@ -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 {

View file

@ -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',
}),
});

View file

@ -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;
};

View file

@ -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 = {

View file

@ -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';

View file

@ -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';

View file

@ -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

View file

@ -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();

View file

@ -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>
)}

View file

@ -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',

View file

@ -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(), []);

View file

@ -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 {
/**

View file

@ -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';

View file

@ -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,

View file

@ -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 {

View file

@ -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,
};
}

View file

@ -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';

View file

@ -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,
});
};

View file

@ -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,
}),
});
};

View file

@ -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}`;
}
};

View file

@ -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();
});
});
});

View file

@ -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,
};
};

View file

@ -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', () => {

View file

@ -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,
};
};

View file

@ -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

View file

@ -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",

View file

@ -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": "説明",

View file

@ -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": "描述",