From 32a45ba742c68cf7bda98abbb411b9d8a2923daa Mon Sep 17 00:00:00 2001 From: machadoum Date: Mon, 18 Dec 2023 16:14:20 +0100 Subject: [PATCH] Create new host details flyout * Refactor user flyout components to be reused by hosts --- .../common/experimental_features.ts | 8 +- .../security_solution/common/utility_types.ts | 3 + .../entity_details_flyout/index.tsx | 4 +- .../hosts/containers/hosts/details/index.tsx | 4 +- .../host_details_left/index.tsx | 49 ++++++ .../host_right/content.stories.tsx | 147 ++++++++++++++++++ .../entity_details/host_right/content.tsx | 54 +++++++ .../host_right/fields/basic_host_fields.tsx | 80 ++++++++++ .../host_right/fields/cloud_fields.ts | 34 ++++ .../fields/endpoint_policy_fields.tsx | 71 +++++++++ .../host_right/fields/translations.ts | 127 +++++++++++++++ .../entity_details/host_right/header.test.tsx | 137 ++++++++++++++++ .../entity_details/host_right/header.tsx | 67 ++++++++ .../host_right/hooks/use_observed_host.ts | 71 +++++++++ .../hooks/use_observed_host_fields.test.ts | 57 +++++++ .../hooks/use_observed_host_fields.ts | 36 +++++ .../entity_details/host_right/index.test.tsx | 110 +++++++++++++ .../entity_details/host_right/index.tsx | 130 ++++++++++++++++ .../flyout/entity_details/mocks/index.ts | 51 ++++++ .../flyout/entity_details/shared/constants.ts | 8 + .../shared/entity_table/columns.tsx | 61 ++++++++ .../shared/entity_table/index.tsx | 45 ++++++ .../shared/entity_table/types.ts | 54 +++++++ .../left_panel/left_panel_content.tsx} | 12 +- .../left_panel/left_panel_header.tsx} | 28 +++- .../shared/observed_entity/index.test.tsx} | 20 +-- .../shared/observed_entity/index.tsx} | 68 ++++---- .../shared/observed_entity/translations.ts | 15 ++ .../shared/observed_entity/types.ts | 27 ++++ .../user_details_left/index.tsx | 19 ++- .../entity_details/user_details_left/tabs.tsx | 20 +-- .../entity_details/user_right/content.tsx | 26 ++-- .../entity_details/user_right/header.tsx | 9 +- .../user_right/hooks/translations.ts | 51 ++++++ .../user_right}/hooks/use_observed_user.ts | 34 ++-- .../hooks/use_observed_user_items.test.ts | 4 +- .../hooks/use_observed_user_items.ts | 69 ++++++++ .../entity_details/user_right/index.tsx | 6 +- .../entity_details/user_right/mocks/index.ts | 68 ++++---- .../security_solution/public/flyout/index.tsx | 15 ++ .../components/host_overview/index.tsx | 1 - .../components/side_panel/common.tsx | 26 ++++ .../new_host_detail/__mocks__/index.ts | 69 ++++++++ .../new_host_detail/translations.ts | 66 ++++++++ .../new_user_detail/__mocks__/index.ts | 40 +---- .../new_user_detail/anomalies_field.tsx | 45 ++++++ .../side_panel/new_user_detail/columns.tsx | 89 +---------- .../hooks/use_observed_user_items.ts | 48 ------ .../new_user_detail/risk_score_field.test.tsx | 46 ------ .../new_user_detail/risk_score_field.tsx | 79 ---------- .../new_user_detail/translations.ts | 50 ------ .../side_panel/new_user_detail/types.ts | 27 ---- .../timeline/body/renderers/host_name.tsx | 70 ++++++--- 53 files changed, 1990 insertions(+), 565 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.stories.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/basic_host_fields.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/cloud_fields.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/endpoint_policy_fields.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/translations.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_right/header.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_right/header.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host_fields.test.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host_fields.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/mocks/index.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/shared/constants.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/shared/entity_table/columns.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/shared/entity_table/index.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/shared/entity_table/types.ts rename x-pack/plugins/security_solution/public/flyout/entity_details/{user_details_left/content.tsx => shared/left_panel/left_panel_content.tsx} (70%) rename x-pack/plugins/security_solution/public/flyout/entity_details/{user_details_left/header.tsx => shared/left_panel/left_panel_header.tsx} (67%) rename x-pack/plugins/security_solution/public/{timelines/components/side_panel/new_user_detail/observed_user.test.tsx => flyout/entity_details/shared/observed_entity/index.test.tsx} (62%) rename x-pack/plugins/security_solution/public/{timelines/components/side_panel/new_user_detail/observed_user.tsx => flyout/entity_details/shared/observed_entity/index.tsx} (55%) create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/translations.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/types.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/translations.ts rename x-pack/plugins/security_solution/public/{timelines/components/side_panel/new_user_detail => flyout/entity_details/user_right}/hooks/use_observed_user.ts (65%) rename x-pack/plugins/security_solution/public/{timelines/components/side_panel/new_user_detail => flyout/entity_details/user_right}/hooks/use_observed_user_items.test.ts (93%) create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/common.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/new_host_detail/__mocks__/index.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/new_host_detail/translations.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/anomalies_field.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_observed_user_items.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/risk_score_field.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/risk_score_field.tsx diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 85525ff82bc1e..902ec774d192a 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -105,11 +105,17 @@ export const allowedExperimentalValues = Object.freeze({ assistantRagOnAlerts: false, /* - * Enables the new user details flyout displayed on the Alerts page and timeline. + * Enables the new user details flyout displayed on the Alerts table. * **/ newUserDetailsFlyout: false, + /* + * Enables the new host details flyout displayed on the Alerts table. + * + **/ + newHostDetailsFlyout: false, + /** * Enable risk engine client and initialisation of datastream, component templates and mappings */ diff --git a/x-pack/plugins/security_solution/common/utility_types.ts b/x-pack/plugins/security_solution/common/utility_types.ts index 1a85597e84e77..4db1808e368af 100644 --- a/x-pack/plugins/security_solution/common/utility_types.ts +++ b/x-pack/plugins/security_solution/common/utility_types.ts @@ -71,3 +71,6 @@ export const assertUnreachable = ( ): never => { throw new Error(`${message}: ${x}`); }; + +type Without = { [P in Exclude]?: never }; +export type XOR = T | U extends object ? (Without & U) | (Without & T) : T | U; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx index 428f98530d55f..d4966db527879 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx @@ -7,14 +7,14 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared/left_panel/left_panel_header'; import { PREFIX } from '../../../flyout/shared/test_ids'; -import { UserDetailsLeftPanelTab } from '../../../flyout/entity_details/user_details_left/tabs'; import { RiskInputsTab } from './tabs/risk_inputs'; export const RISK_INPUTS_TAB_TEST_ID = `${PREFIX}RiskInputsTab` as const; export const getRiskInputTab = (alertIds: string[]) => ({ - id: UserDetailsLeftPanelTab.RISK_INPUTS, + id: EntityDetailsLeftPanelTab.RISK_INPUTS, 'data-test-subj': RISK_INPUTS_TAB_TEST_ID, name: ( { +}: UseHostDetails): [boolean, HostDetailsArgs, inputsModel.Refetch] => { const { loading, result: response, @@ -91,5 +91,5 @@ export const useHostDetails = ({ } }, [hostDetailsRequest, search, skip]); - return [loading, hostDetailsResponse]; + return [loading, hostDetailsResponse, refetch]; }; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx new file mode 100644 index 0000000000000..dcc15768309cb --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; +import { getRiskInputTab } from '../../../entity_analytics/components/entity_details_flyout'; +import { EntityDetailsLeftPanelTab, LeftPanelHeader } from '../shared/left_panel/left_panel_header'; +import { LeftPanelContent } from '../shared/left_panel/left_panel_content'; + +interface RiskInputsParam { + alertIds: string[]; +} + +export interface HostDetailsPanelProps extends Record { + riskInputs: RiskInputsParam; +} +export interface HostDetailsExpandableFlyoutProps extends FlyoutPanelProps { + key: 'host_details'; + params: HostDetailsPanelProps; +} +export const HostDetailsPanelKey: HostDetailsExpandableFlyoutProps['key'] = 'host_details'; + +export const HostDetailsPanel = ({ riskInputs }: HostDetailsPanelProps) => { + // Temporary implementation while Host details left panel don't have Asset tabs + const [tabs, selectedTabId, setSelectedTabId] = useMemo(() => { + return [ + riskInputs.alertIds.length > 0 ? [getRiskInputTab(riskInputs.alertIds)] : [], + EntityDetailsLeftPanelTab.RISK_INPUTS, + () => {}, + ]; + }, [riskInputs.alertIds]); + + return ( + <> + + + + ); +}; + +HostDetailsPanel.displayName = 'HostDetailsPanel'; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.stories.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.stories.tsx new file mode 100644 index 0000000000000..b8975b747ea44 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.stories.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { EuiFlyout } from '@elastic/eui'; +import type { ExpandableFlyoutContextValue } from '@kbn/expandable-flyout/src/context'; +import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; +import { StorybookProviders } from '../../../common/mock/storybook_providers'; +import { + mockManagedUserData, + mockObservedUser, + mockRiskScoreState, +} from '../../../timelines/components/side_panel/new_user_detail/__mocks__'; +import { UserPanelContent } from './content'; + +const flyoutContextValue = { + openLeftPanel: () => window.alert('openLeftPanel called'), + panels: {}, +} as unknown as ExpandableFlyoutContextValue; + +const riskScoreData = { ...mockRiskScoreState, data: [] }; + +storiesOf('Components/UserPanelContent', module) + .addDecorator((storyFn) => ( + + + {}}> + {storyFn()} + + + + )) + .add('default', () => ( + + )) + .add('integration disabled', () => ( + + )) + .add('no managed data', () => ( + + )) + .add('no observed data', () => ( + + )) + .add('loading', () => ( + + )); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx new file mode 100644 index 0000000000000..7de3df63dc1ba --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx @@ -0,0 +1,54 @@ +/* + * 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 { EuiHorizontalRule } from '@elastic/eui'; + +import React from 'react'; +import type { RiskScoreState } from '../../../entity_analytics/api/hooks/use_risk_score'; +import type { HostItem, RiskScoreEntity } from '../../../../common/search_strategy'; +import { FlyoutBody } from '../../shared/components/flyout_body'; +import { ObservedEntity } from '../shared/observed_entity'; +import { HOST_PANEL_OBSERVED_HOST_QUERY_ID } from '.'; +import type { ObservedEntityData } from '../shared/observed_entity/types'; +import { useObservedHostFields } from './hooks/use_observed_host_fields'; + +interface HostPanelContentProps { + observedHost: ObservedEntityData; + riskScoreState: RiskScoreState; + contextID: string; + scopeId: string; + isDraggable: boolean; +} + +export const HostPanelContent = ({ + observedHost, + riskScoreState, + contextID, + scopeId, + isDraggable, +}: HostPanelContentProps) => { + const observedFields = useObservedHostFields(observedHost); + + return ( + + {riskScoreState.isModuleEnabled && riskScoreState.data?.length !== 0 && ( + <> + {/* TODO */} + + + )} + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/basic_host_fields.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/basic_host_fields.tsx new file mode 100644 index 0000000000000..d34daa8243d1d --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/basic_host_fields.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { getEmptyTagValue } from '../../../../common/components/empty_value'; +import type { HostItem } from '../../../../../common/search_strategy'; +import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; +import { NetworkDetailsLink } from '../../../../common/components/links'; +import * as i18n from './translations'; +import type { ObservedEntityData } from '../../shared/observed_entity/types'; +import type { EntityTableRows } from '../../shared/entity_table/types'; + +export const basicHostFields: EntityTableRows> = [ + { + label: i18n.HOST_ID, + getValues: (hostData: ObservedEntityData) => hostData.details.host?.id, + field: 'host.id', + }, + { + label: i18n.FIRST_SEEN, + render: (hostData: ObservedEntityData) => + hostData.firstSeen.date ? ( + + ) : ( + getEmptyTagValue() + ), + }, + { + label: i18n.LAST_SEEN, + render: (hostData: ObservedEntityData) => + hostData.lastSeen.date ? ( + + ) : ( + getEmptyTagValue() + ), + }, + { + label: i18n.IP_ADDRESSES, + field: 'host.ip', + getValues: (hostData: ObservedEntityData) => hostData.details.host?.ip, + renderField: (ip: string) => { + return ; + }, + }, + { + label: i18n.MAC_ADDRESSES, + getValues: (hostData: ObservedEntityData) => hostData.details.host?.mac, + field: 'host.mac', + }, + { + label: i18n.PLATFORM, + getValues: (hostData: ObservedEntityData) => hostData.details.host?.os?.platform, + field: 'host.os.platform', + }, + { + label: i18n.OS, + getValues: (hostData: ObservedEntityData) => hostData.details.host?.os?.name, + field: 'host.os.name', + }, + { + label: i18n.FAMILY, + getValues: (hostData: ObservedEntityData) => hostData.details.host?.os?.family, + field: 'host.os.family', + }, + { + label: i18n.VERSION, + getValues: (hostData: ObservedEntityData) => hostData.details.host?.os?.version, + field: 'host.os.version', + }, + { + label: i18n.ARCHITECTURE, + getValues: (hostData: ObservedEntityData) => hostData.details.host?.architecture, + field: 'host.architecture', + }, +]; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/cloud_fields.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/cloud_fields.ts new file mode 100644 index 0000000000000..435fa3fd53dcf --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/cloud_fields.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HostItem } from '../../../../../common/search_strategy'; +import type { EntityTableRows } from '../../shared/entity_table/types'; +import type { ObservedEntityData } from '../../shared/observed_entity/types'; +import * as i18n from './translations'; + +export const cloudFields: EntityTableRows> = [ + { + label: i18n.CLOUD_PROVIDER, + getValues: (hostData: ObservedEntityData) => hostData.details.cloud?.provider, + field: 'cloud.provider', + }, + { + label: i18n.REGION, + getValues: (hostData: ObservedEntityData) => hostData.details.cloud?.region, + field: 'cloud.region', + }, + { + label: i18n.INSTANCE_ID, + getValues: (hostData: ObservedEntityData) => hostData.details.cloud?.instance?.id, + field: 'cloud.instance.id', + }, + { + label: i18n.MACHINE_TYPE, + getValues: (hostData: ObservedEntityData) => hostData.details.cloud?.machine?.type, + field: 'cloud.machine.type', + }, +]; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/endpoint_policy_fields.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/endpoint_policy_fields.tsx new file mode 100644 index 0000000000000..b1a7ac9dc8f29 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/endpoint_policy_fields.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiHealth } from '@elastic/eui'; + +import type { EntityTableRows } from '../../shared/entity_table/types'; +import type { ObservedEntityData } from '../../shared/observed_entity/types'; +import { EndpointAgentStatus } from '../../../../common/components/endpoint/endpoint_agent_status'; +import { getEmptyTagValue } from '../../../../common/components/empty_value'; +import type { HostItem } from '../../../../../common/search_strategy'; +import { HostPolicyResponseActionStatus } from '../../../../../common/search_strategy'; +import * as i18n from './translations'; + +export const policyFields: EntityTableRows> = [ + { + label: i18n.ENDPOINT_POLICY, + render: (hostData: ObservedEntityData) => { + const appliedPolicy = hostData.details.endpoint?.hostInfo?.metadata.Endpoint.policy.applied; + return appliedPolicy?.name ? <>{appliedPolicy.name} : getEmptyTagValue(); + }, + isVisible: (hostData: ObservedEntityData) => hostData.details.endpoint != null, + }, + { + label: i18n.POLICY_STATUS, + render: (hostData: ObservedEntityData) => { + const appliedPolicy = hostData.details.endpoint?.hostInfo?.metadata.Endpoint.policy.applied; + return appliedPolicy?.status ? ( + + {appliedPolicy?.status} + + ) : ( + getEmptyTagValue() + ); + }, + isVisible: (hostData: ObservedEntityData) => hostData.details.endpoint != null, + }, + { + label: i18n.SENSORVERSION, + getValues: (hostData: ObservedEntityData) => + hostData.details.endpoint?.hostInfo?.metadata.agent.version + ? [hostData.details.endpoint?.hostInfo?.metadata.agent.version] + : undefined, + field: 'agent.version', + isVisible: (hostData: ObservedEntityData) => hostData.details.endpoint != null, + }, + { + label: i18n.FLEET_AGENT_STATUS, + render: (hostData: ObservedEntityData) => + hostData.details.endpoint?.hostInfo ? ( + + ) : ( + getEmptyTagValue() + ), + isVisible: (hostData: ObservedEntityData) => hostData.details.endpoint != null, + }, +]; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/translations.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/translations.ts new file mode 100644 index 0000000000000..63ac4faf9c401 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/translations.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const HOST_ID = i18n.translate('xpack.securitySolution.host.details.overview.hostIdTitle', { + defaultMessage: 'Host ID', +}); + +export const FIRST_SEEN = i18n.translate('xpack.securitySolution.host.details.firstSeenTitle', { + defaultMessage: 'First seen', +}); + +export const LAST_SEEN = i18n.translate('xpack.securitySolution.host.details.lastSeenTitle', { + defaultMessage: 'Last seen', +}); + +export const HOST_RISK_SCORE = i18n.translate( + 'xpack.securitySolution.host.details.overview.hostRiskScoreTitle', + { + defaultMessage: 'Host risk score', + } +); + +export const HOST_RISK_LEVEL = i18n.translate( + 'xpack.securitySolution.host.details.overview.hostRiskLevel', + { + defaultMessage: 'Host risk level', + } +); + +export const IP_ADDRESSES = i18n.translate( + 'xpack.securitySolution.host.details.overview.ipAddressesTitle', + { + defaultMessage: 'IP addresses', + } +); + +export const MAC_ADDRESSES = i18n.translate( + 'xpack.securitySolution.host.details.overview.macAddressesTitle', + { + defaultMessage: 'MAC addresses', + } +); + +export const PLATFORM = i18n.translate( + 'xpack.securitySolution.host.details.overview.platformTitle', + { + defaultMessage: 'Platform', + } +); + +export const OS = i18n.translate('xpack.securitySolution.host.details.overview.osTitle', { + defaultMessage: 'Operating system', +}); + +export const FAMILY = i18n.translate('xpack.securitySolution.host.details.overview.familyTitle', { + defaultMessage: 'Family', +}); + +export const VERSION = i18n.translate('xpack.securitySolution.host.details.versionLabel', { + defaultMessage: 'Version', +}); + +export const ARCHITECTURE = i18n.translate( + 'xpack.securitySolution.host.details.architectureLabel', + { + defaultMessage: 'Architecture', + } +); + +export const CLOUD_PROVIDER = i18n.translate( + 'xpack.securitySolution.host.details.overview.cloudProviderTitle', + { + defaultMessage: 'Cloud provider', + } +); + +export const REGION = i18n.translate('xpack.securitySolution.host.details.overview.regionTitle', { + defaultMessage: 'Region', +}); + +export const INSTANCE_ID = i18n.translate( + 'xpack.securitySolution.host.details.overview.instanceIdTitle', + { + defaultMessage: 'Instance ID', + } +); + +export const MACHINE_TYPE = i18n.translate( + 'xpack.securitySolution.host.details.overview.machineTypeTitle', + { + defaultMessage: 'Machine type', + } +); + +export const ENDPOINT_POLICY = i18n.translate( + 'xpack.securitySolution.host.details.endpoint.endpointPolicy', + { + defaultMessage: 'Endpoint integration policy', + } +); + +export const POLICY_STATUS = i18n.translate( + 'xpack.securitySolution.host.details.endpoint.policyStatus', + { + defaultMessage: 'Policy Status', + } +); + +export const SENSORVERSION = i18n.translate( + 'xpack.securitySolution.host.details.endpoint.sensorversion', + { + defaultMessage: 'Endpoint version', + } +); + +export const FLEET_AGENT_STATUS = i18n.translate( + 'xpack.securitySolution.host.details.endpoint.fleetAgentStatus', + { + defaultMessage: 'Agent status', + } +); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/header.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/header.test.tsx new file mode 100644 index 0000000000000..2f1ada7447dd6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/header.test.tsx @@ -0,0 +1,137 @@ +/* + * 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 { ManagedUserDatasetKey } from '../../../../common/search_strategy/security_solution/users/managed_details'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { TestProviders } from '../../../common/mock'; +import { + managedUserDetails, + mockManagedUserData, + mockObservedUser, +} from '../../../timelines/components/side_panel/new_user_detail/__mocks__'; +import { UserPanelHeader } from './header'; + +const mockProps = { + userName: 'test', + managedUser: mockManagedUserData, + observedUser: mockObservedUser, +}; + +jest.mock('../../../common/components/visualization_actions/visualization_embeddable'); + +describe('UserDetailsContent', () => { + it('renders', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('user-panel-header')).toBeInTheDocument(); + }); + + it('renders observed user date when it is bigger than managed user date', () => { + const futureDay = '2989-03-07T20:00:00.000Z'; + const { getByTestId } = render( + + + + ); + + expect(getByTestId('user-panel-header-lastSeen').textContent).toContain('Mar 7, 2989'); + }); + + it('renders managed user date when it is bigger than observed user date', () => { + const futureDay = '2989-03-07T20:00:00.000Z'; + const entraManagedUser = managedUserDetails[ManagedUserDatasetKey.ENTRA]!; + const { getByTestId } = render( + + + + ); + + expect(getByTestId('user-panel-header-lastSeen').textContent).toContain('Mar 7, 2989'); + }); + + it('renders observed and managed badges when lastSeen is defined', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('user-panel-header-observed-badge')).toBeInTheDocument(); + expect(getByTestId('user-panel-header-managed-badge')).toBeInTheDocument(); + }); + + it('does not render observed badge when lastSeen date is undefined', () => { + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('user-panel-header-observed-badge')).not.toBeInTheDocument(); + }); + + it('does not render managed badge when managed data is undefined', () => { + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('user-panel-header-managed-badge')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/header.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/header.tsx new file mode 100644 index 0000000000000..2d2b32c6323d4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/header.tsx @@ -0,0 +1,67 @@ +/* + * 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 { EuiSpacer, EuiBadge, EuiText, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useMemo } from 'react'; +import { SecurityPageName } from '@kbn/security-solution-navigation'; +import type { HostItem } from '../../../../common/search_strategy'; +import { getHostDetailsUrl } from '../../../common/components/link_to'; +import { SecuritySolutionLinkAnchor } from '../../../common/components/links'; +import { PreferenceFormattedDate } from '../../../common/components/formatted_date'; +import { FlyoutHeader } from '../../shared/components/flyout_header'; +import { FlyoutTitle } from '../../shared/components/flyout_title'; +import type { ObservedEntityData } from '../shared/observed_entity/types'; + +interface HostPanelHeaderProps { + hostName: string; + observedHost: ObservedEntityData; +} + +export const HostPanelHeader = ({ hostName, observedHost }: HostPanelHeaderProps) => { + const lastSeenDate = useMemo( + () => observedHost.lastSeen.date && new Date(observedHost.lastSeen.date), + [observedHost.lastSeen.date] + ); + + return ( + + + + + {lastSeenDate && } + + + + + + + + + + + + {observedHost.lastSeen.date && ( + + + + )} + + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host.ts new file mode 100644 index 0000000000000..eb7e467b1d9da --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useHostDetails } from '../../../../explore/hosts/containers/hosts/details'; +import { useFirstLastSeen } from '../../../../common/containers/use_first_last_seen'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { useSourcererDataView } from '../../../../common/containers/sourcerer'; +import type { HostItem } from '../../../../../common/search_strategy'; +import { Direction, NOT_EVENT_KIND_ASSET_FILTER } from '../../../../../common/search_strategy'; +import { HOST_PANEL_OBSERVED_HOST_QUERY_ID, HOST_PANEL_RISK_SCORE_QUERY_ID } from '..'; +import { useQueryInspector } from '../../../../common/components/page/manage_query'; +import type { ObservedEntityData } from '../../shared/observed_entity/types'; + +export const useObservedHost = ( + hostName: string +): Omit, 'anomalies'> => { + const { to, from, isInitializing, setQuery, deleteQuery } = useGlobalTime(); + const { selectedPatterns } = useSourcererDataView(); + + const [isLoading, { hostDetails, inspect: inspectObservedHost }, refetch] = useHostDetails({ + endDate: to, + hostName, + indexNames: selectedPatterns, + id: HOST_PANEL_RISK_SCORE_QUERY_ID, + skip: isInitializing, + startDate: from, + }); + + useQueryInspector({ + deleteQuery, + inspect: inspectObservedHost, + loading: isLoading, + queryId: HOST_PANEL_OBSERVED_HOST_QUERY_ID, + refetch, + setQuery, + }); + + const [loadingFirstSeen, { firstSeen }] = useFirstLastSeen({ + field: 'host.name', + value: hostName, + defaultIndex: selectedPatterns, + order: Direction.asc, + filterQuery: NOT_EVENT_KIND_ASSET_FILTER, + }); + + const [loadingLastSeen, { lastSeen }] = useFirstLastSeen({ + field: 'host.name', + value: hostName, + defaultIndex: selectedPatterns, + order: Direction.desc, + filterQuery: NOT_EVENT_KIND_ASSET_FILTER, + }); + + return useMemo( + () => ({ + details: hostDetails, + isLoading: isLoading && loadingLastSeen && loadingFirstSeen, + firstSeen: { + date: firstSeen, + isLoading: loadingFirstSeen, + }, + lastSeen: { date: lastSeen, isLoading: loadingLastSeen }, + }), + [firstSeen, hostDetails, isLoading, lastSeen, loadingFirstSeen, loadingLastSeen] + ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host_fields.test.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host_fields.test.ts new file mode 100644 index 0000000000000..3adcafabbabc3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host_fields.test.ts @@ -0,0 +1,57 @@ +/* + * 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 { mockObservedHostData } from '../../../../timelines/components/side_panel/new_host_detail/__mocks__'; +import { renderHook } from '@testing-library/react-hooks'; +import { useObservedHostFields } from './use_observed_host_fields'; +import { TestProviders } from '@kbn/timelines-plugin/public/mock'; + +describe('useManagedUserItems', () => { + it('returns managed user items for Entra user', () => { + const { result } = renderHook(() => useObservedHostFields(mockObservedHostData), { + wrapper: TestProviders, + }); + + expect(result.current).toEqual([ + { + field: 'user.id', + label: 'User ID', + values: ['1234', '321'], + }, + { + field: 'user.domain', + label: 'Domain', + values: ['test domain', 'another test domain'], + }, + { + field: '@timestamp', + label: 'First seen', + values: ['2023-02-23T20:03:17.489Z'], + }, + { + field: '@timestamp', + label: 'Last seen', + values: ['2023-02-23T20:03:17.489Z'], + }, + { + field: 'host.os.name', + label: 'Operating system', + values: ['testOs'], + }, + { + field: 'host.os.family', + label: 'Family', + values: ['testFamily'], + }, + { + field: 'host.ip', + label: 'IP addresses', + values: ['10.0.0.1', '127.0.0.1'], + }, + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host_fields.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host_fields.ts new file mode 100644 index 0000000000000..8996ee2a17add --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/hooks/use_observed_host_fields.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities'; +import type { HostItem } from '../../../../../common/search_strategy'; +import { getAnomaliesFields } from '../../../../timelines/components/side_panel/common'; +import type { EntityTableRows } from '../../shared/entity_table/types'; +import type { ObservedEntityData } from '../../shared/observed_entity/types'; +import { policyFields } from '../fields/endpoint_policy_fields'; +import { basicHostFields } from '../fields/basic_host_fields'; +import { cloudFields } from '../fields/cloud_fields'; + +export const useObservedHostFields = ( + hostData: ObservedEntityData +): EntityTableRows> => { + const mlCapabilities = useMlCapabilities(); + + const fields: EntityTableRows> = useMemo( + () => [ + ...basicHostFields, + ...getAnomaliesFields(mlCapabilities), + ...cloudFields, + ...policyFields, + ], + [mlCapabilities] + ); + + if (!hostData.details) return []; + + return fields; +}; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.test.tsx new file mode 100644 index 0000000000000..dfc29431485de --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.test.tsx @@ -0,0 +1,110 @@ +/* + * 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 { render } from '@testing-library/react'; +import React from 'react'; +import { TestProviders } from '../../../common/mock'; +import { mockRiskScoreState } from '../mocks'; + +import { mockManagedUserData } from '../../../timelines/components/side_panel/new_user_detail/__mocks__'; +import type { UserPanelProps } from '../user_right'; +import { UserPanel } from '../user_right'; +import { mockObservedUser } from '../user_right/mocks'; + +const mockProps: UserPanelProps = { + userName: 'test', + contextID: 'test-user-panel', + scopeId: 'test-scope-id', + isDraggable: false, +}; + +jest.mock('../../../common/components/visualization_actions/visualization_embeddable'); + +const mockedUseRiskScore = jest.fn().mockReturnValue(mockRiskScoreState); +jest.mock('../../../entity_analytics/api/hooks/use_risk_score', () => ({ + useRiskScore: () => mockedUseRiskScore(), +})); + +const mockedUseManagedUser = jest.fn().mockReturnValue(mockManagedUserData); +const mockedUseObservedUser = jest.fn().mockReturnValue(mockObservedUser); + +jest.mock( + '../../../timelines/components/side_panel/new_user_detail/hooks/use_managed_user', + () => ({ + useManagedUser: () => mockedUseManagedUser(), + }) +); + +jest.mock('./hooks/use_observed_host', () => ({ + useObservedUser: () => mockedUseObservedUser(), +})); + +describe('UserPanel', () => { + beforeEach(() => { + mockedUseRiskScore.mockReturnValue(mockRiskScoreState); + mockedUseManagedUser.mockReturnValue(mockManagedUserData); + mockedUseObservedUser.mockReturnValue(mockObservedUser); + }); + + it('renders', () => { + const { getByTestId, queryByTestId } = render( + + + + ); + + expect(getByTestId('user-panel-header')).toBeInTheDocument(); + expect(queryByTestId('securitySolutionFlyoutLoading')).not.toBeInTheDocument(); + expect(getByTestId('securitySolutionFlyoutNavigationExpandDetailButton')).toBeInTheDocument(); + }); + + it('renders loading state when risk score is loading', () => { + mockedUseRiskScore.mockReturnValue({ + ...mockRiskScoreState, + data: undefined, + loading: true, + }); + + const { getByTestId } = render( + + + + ); + + expect(getByTestId('securitySolutionFlyoutLoading')).toBeInTheDocument(); + }); + + it('renders loading state when observed user is loading', () => { + mockedUseObservedUser.mockReturnValue({ + ...mockObservedUser, + isLoading: true, + }); + + const { getByTestId } = render( + + + + ); + + expect(getByTestId('securitySolutionFlyoutLoading')).toBeInTheDocument(); + }); + + it('renders loading state when managed user is loading', () => { + mockedUseManagedUser.mockReturnValue({ + ...mockManagedUserData, + isLoading: true, + }); + + const { getByTestId } = render( + + + + ); + + expect(getByTestId('securitySolutionFlyoutLoading')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx new file mode 100644 index 0000000000000..054096d194148 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; +import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { hostToCriteria } from '../../../common/components/ml/criteria/host_to_criteria'; +import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; +import { useQueryInspector } from '../../../common/components/page/manage_query'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; +import type { HostItem } from '../../../../common/search_strategy'; +import { buildHostNamesFilter } from '../../../../common/search_strategy'; +import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine'; +import { FlyoutLoading } from '../../shared/components/flyout_loading'; +import { FlyoutNavigation } from '../../shared/components/flyout_navigation'; +import { HostPanelContent } from './content'; +import { HostPanelHeader } from './header'; +import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anomaly_table_provider'; +import type { ObservedEntityData } from '../shared/observed_entity/types'; +import { useObservedHost } from './hooks/use_observed_host'; +import { HostDetailsPanelKey } from '../host_details_left'; + +export interface HostPanelProps extends Record { + contextID: string; + scopeId: string; + hostName: string; + isDraggable?: boolean; +} + +export interface HostPanelExpandableFlyoutProps extends FlyoutPanelProps { + key: 'host-panel'; + params: HostPanelProps; +} + +export const HostPanelKey: HostPanelExpandableFlyoutProps['key'] = 'host-panel'; +export const HOST_PANEL_RISK_SCORE_QUERY_ID = 'HostPanelRiskScoreQuery'; +export const HOST_PANEL_OBSERVED_HOST_QUERY_ID = 'HostPanelObservedHostQuery'; + +const FIRST_RECORD_PAGINATION = { + cursorStart: 0, + querySize: 1, +}; + +export const HostPanel = ({ contextID, scopeId, hostName, isDraggable }: HostPanelProps) => { + const { openLeftPanel } = useExpandableFlyoutContext(); + const { to, from, isInitializing, setQuery, deleteQuery } = useGlobalTime(); + const hostNameFilterQuery = useMemo( + () => (hostName ? buildHostNamesFilter([hostName]) : undefined), + [hostName] + ); + + const riskScoreState = useRiskScore({ + riskEntity: RiskScoreEntity.host, + filterQuery: hostNameFilterQuery, + onlyLatest: false, + pagination: FIRST_RECORD_PAGINATION, + }); + + const { data: hostRisk, inspect: inspectRiskScore, refetch, loading } = riskScoreState; + const hostRiskData = hostRisk && hostRisk.length > 0 ? hostRisk[0] : undefined; + + useQueryInspector({ + deleteQuery, + inspect: inspectRiskScore, + loading, + queryId: HOST_PANEL_RISK_SCORE_QUERY_ID, + refetch, + setQuery, + }); + + const openPanel = useCallback(() => { + openLeftPanel({ + id: HostDetailsPanelKey, + params: { + riskInputs: { + alertIds: hostRiskData?.host.risk.inputs?.map(({ id }) => id) ?? [], + }, + }, + }); + }, [openLeftPanel, hostRiskData?.host.risk.inputs]); + + const useObserved = useObservedHost(hostName); + + if (riskScoreState.loading || useObserved.isLoading) { + return ; + } + + return ( + + {({ isLoadingAnomaliesData, anomaliesData, jobNameById }) => { + const observedHostWithAnomalies: ObservedEntityData = { + ...useObserved, + anomalies: { + isLoading: isLoadingAnomaliesData, + anomalies: anomaliesData, + jobNameById, + }, + }; + + return ( + <> + + + + + ); + }} + + ); +}; + +HostPanel.displayName = 'HostPanel'; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/mocks/index.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/mocks/index.ts new file mode 100644 index 0000000000000..b2263413c6b36 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/mocks/index.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RiskScoreState } from '../../../entity_analytics/api/hooks/use_risk_score'; +import type { RiskScoreEntity, UserRiskScore } from '../../../../common/search_strategy'; +import { RiskSeverity } from '../../../../common/search_strategy'; +import { RiskCategories } from '../../../../common/entity_analytics/risk_engine'; + +const userRiskScore: UserRiskScore = { + '@timestamp': '626569200000', + user: { + name: 'test', + risk: { + rule_risks: [], + calculated_score_norm: 70, + multipliers: [], + calculated_level: RiskSeverity.high, + inputs: [ + { + id: '_id', + index: '_index', + category: RiskCategories.category_1, + description: 'Alert from Rule: My rule', + risk_score: 30, + timestamp: '2021-08-19T18:55:59.000Z', + }, + ], + }, + }, + alertsCount: 0, + oldestAlertTimestamp: '626569200000', +}; + +export const mockRiskScoreState: RiskScoreState = { + data: [userRiskScore], + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + refetch: () => {}, + totalCount: 0, + isModuleEnabled: true, + isAuthorized: true, + isDeprecated: false, + loading: false, +}; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/constants.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/constants.ts new file mode 100644 index 0000000000000..bad35d3657891 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/constants.ts @@ -0,0 +1,8 @@ +/* + * 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 const ONE_WEEK_IN_HOURS = 24 * 7; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/entity_table/columns.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/entity_table/columns.tsx new file mode 100644 index 0000000000000..8324a7e1308b0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/entity_table/columns.tsx @@ -0,0 +1,61 @@ +/* + * 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 { css } from '@emotion/react'; +import React from 'react'; +import { euiLightVars } from '@kbn/ui-theme'; +import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/field_renderers'; +import { getEmptyTagValue } from '../../../../common/components/empty_value'; +import * as i18n from '../../../../timelines/components/side_panel/new_host_detail/translations'; +import { getSourcererScopeId } from '../../../../helpers'; +import type { BasicEntityData, EntityTableColumns } from './types'; + +export const getEntityTableColumns = ( + contextID: string, + scopeId: string, + isDraggable: boolean, + data: T +): EntityTableColumns => [ + { + name: i18n.FIELD_COLUMN_TITLE, + field: 'label', + render: (label: string, { field }) => ( + + {label ?? field} + + ), + }, + { + name: i18n.VALUES_COLUMN_TITLE, + field: 'field', + render: (field: string | undefined, { getValues, render, renderField }) => { + const values = getValues && getValues(data); + + if (field) { + return ( + + ); + } + + if (render) return render(data); + + return getEmptyTagValue(); + }, + }, +]; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/entity_table/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/entity_table/index.tsx new file mode 100644 index 0000000000000..1a4c23beffef3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/entity_table/index.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { BasicTable } from '../../../../common/components/ml/tables/basic_table'; +import { getEntityTableColumns } from './columns'; +import type { BasicEntityData, EntityTableRows } from './types'; + +interface EntityTableProps { + contextID: string; + scopeId: string; + isDraggable: boolean; + data: T; + entityFields: EntityTableRows; +} + +export const EntityTable = ({ + contextID, + scopeId, + isDraggable, + data, + entityFields, +}: EntityTableProps) => { + const items = useMemo( + () => entityFields.filter(({ isVisible }) => (isVisible ? isVisible(data) : true)), + [data, entityFields] + ); + + const entityTableColumns = useMemo( + () => getEntityTableColumns(contextID, scopeId, isDraggable, data), + [contextID, scopeId, isDraggable, data] + ); + return ( + + ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/entity_table/types.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/entity_table/types.ts new file mode 100644 index 0000000000000..289348fe595de --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/entity_table/types.ts @@ -0,0 +1,54 @@ +/* + * 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 { EuiBasicTableColumn } from '@elastic/eui'; +import type { XOR } from '../../../../../common/utility_types'; + +export type EntityTableRow = XOR< + { + label: string; + /** + * The field name. It is used for displaying CellActions. + */ + field: string; + /** + * It extracts an array of strings from the data. Each element is a valid field value. + * It is used for displaying MoreContainer. + */ + getValues: (data: T) => string[] | null | undefined; + /** + * It allows the customization of the rendered field. + * The element is still rendered inside `DefaultFieldRenderer` getting `CellActions` and `MoreContainer` capabilities. + */ + renderField?: (value: string) => JSX.Element; + /** + * It hides the row when `isVisible` returns false. + */ + isVisible?: (data: T) => boolean; + }, + { + label: string; + /** + * It takes complete control over the rendering. + * `getValues` and `renderField` are not called when this property is used. + */ + render: (data: T) => JSX.Element; + /** + * It hides the row when `isVisible` returns false. + */ + isVisible?: (data: T) => boolean; + } +>; + +export type EntityTableColumns = Array< + EuiBasicTableColumn> +>; +export type EntityTableRows = Array>; + +export interface BasicEntityData { + isLoading: boolean; +} diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/content.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/left_panel/left_panel_content.tsx similarity index 70% rename from x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/content.tsx rename to x-pack/plugins/security_solution/public/flyout/entity_details/shared/left_panel/left_panel_content.tsx index 991592bd1ea0c..3e21377e70457 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/left_panel/left_panel_content.tsx @@ -9,19 +9,19 @@ import { useEuiBackgroundColor } from '@elastic/eui'; import type { VFC } from 'react'; import React, { useMemo } from 'react'; import { css } from '@emotion/react'; -import type { LeftPanelTabsType, UserDetailsLeftPanelTab } from './tabs'; -import { FlyoutBody } from '../../shared/components/flyout_body'; +import { FlyoutBody } from '../../../shared/components/flyout_body'; +import type { EntityDetailsLeftPanelTab, LeftPanelTabsType } from './left_panel_header'; export interface PanelContentProps { - selectedTabId: UserDetailsLeftPanelTab; + selectedTabId: EntityDetailsLeftPanelTab; tabs: LeftPanelTabsType; } /** - * User details expandable flyout left section. + * Content for a entity left panel. * Appears after the user clicks on the expand details button in the right section. */ -export const PanelContent: VFC = ({ selectedTabId, tabs }) => { +export const LeftPanelContent: VFC = ({ selectedTabId, tabs }) => { const selectedTabContent = useMemo(() => { return tabs.find((tab) => tab.id === selectedTabId)?.content; }, [selectedTabId, tabs]); @@ -37,4 +37,4 @@ export const PanelContent: VFC = ({ selectedTabId, tabs }) => ); }; -PanelContent.displayName = 'PanelContent'; +LeftPanelContent.displayName = 'LeftPanelContent'; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/header.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/left_panel/left_panel_header.tsx similarity index 67% rename from x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/header.tsx rename to x-pack/plugins/security_solution/public/flyout/entity_details/shared/left_panel/left_panel_header.tsx index 2f807ca1d0a7d..684efe2c8c2ee 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/header.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/left_panel/left_panel_header.tsx @@ -6,21 +6,33 @@ */ import { EuiTab, EuiTabs, useEuiBackgroundColor } from '@elastic/eui'; -import type { VFC } from 'react'; +import type { ReactElement, VFC } from 'react'; import React, { memo } from 'react'; import { css } from '@emotion/react'; -import type { LeftPanelTabsType, UserDetailsLeftPanelTab } from './tabs'; -import { FlyoutHeader } from '../../shared/components/flyout_header'; +import { FlyoutHeader } from '../../../shared/components/flyout_header'; + +export type LeftPanelTabsType = Array<{ + id: EntityDetailsLeftPanelTab; + 'data-test-subj': string; + name: ReactElement; + content: React.ReactElement; +}>; + +export enum EntityDetailsLeftPanelTab { + RISK_INPUTS = 'risk_inputs', + OKTA = 'okta_document', + ENTRA = 'entra_document', +} export interface PanelHeaderProps { /** * Id of the tab selected in the parent component to display its content */ - selectedTabId: UserDetailsLeftPanelTab; + selectedTabId: EntityDetailsLeftPanelTab; /** * Callback to set the selected tab id in the parent component */ - setSelectedTabId: (selected: UserDetailsLeftPanelTab) => void; + setSelectedTabId: (selected: EntityDetailsLeftPanelTab) => void; /** * List of tabs to display in the header */ @@ -31,9 +43,9 @@ export interface PanelHeaderProps { * Header at the top of the left section. * Displays the investigation and insights tabs (visualize is hidden for 8.9). */ -export const PanelHeader: VFC = memo( +export const LeftPanelHeader: VFC = memo( ({ selectedTabId, setSelectedTabId, tabs }) => { - const onSelectedTabChanged = (id: UserDetailsLeftPanelTab) => setSelectedTabId(id); + const onSelectedTabChanged = (id: EntityDetailsLeftPanelTab) => setSelectedTabId(id); const renderTabs = tabs.map((tab, index) => ( onSelectedTabChanged(tab.id)} @@ -61,4 +73,4 @@ export const PanelHeader: VFC = memo( } ); -PanelHeader.displayName = 'PanelHeader'; +LeftPanelHeader.displayName = 'LeftPanelHeader'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/observed_user.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/index.test.tsx similarity index 62% rename from x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/observed_user.test.tsx rename to x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/index.test.tsx index 90934c533a2c4..84ce882d71304 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/observed_user.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/index.test.tsx @@ -7,42 +7,44 @@ import { render } from '@testing-library/react'; import React from 'react'; +import { ObservedEntity } from '.'; import { TestProviders } from '../../../../common/mock'; -import { mockObservedUser } from './__mocks__'; -import { ObservedUser } from './observed_user'; +import { mockObservedHostData } from '../../../../timelines/components/side_panel/new_host_detail/__mocks__'; -describe('ObservedUser', () => { +describe('ObservedHost', () => { const mockProps = { - observedUser: mockObservedUser, + observedData: mockObservedHostData, contextID: '', scopeId: '', isDraggable: false, + queryId: 'TEST_QUERY_ID', + observedFields: [], }; it('renders', () => { const { getByTestId } = render( - + ); - expect(getByTestId('observedUser-data')).toBeInTheDocument(); + expect(getByTestId('observedEntity-data')).toBeInTheDocument(); }); it('renders the formatted date', () => { const { getByTestId } = render( - + ); - expect(getByTestId('observedUser-data')).toHaveTextContent('Updated Feb 23, 2023'); + expect(getByTestId('observedEntity-data')).toHaveTextContent('Updated Feb 23, 2023'); }); it('renders anomaly score', () => { const { getByTestId } = render( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/observed_user.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/index.tsx similarity index 55% rename from x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/observed_user.tsx rename to x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/index.tsx index 411e516b570f3..07b6456f3a45a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/observed_user.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/index.tsx @@ -5,39 +5,35 @@ * 2.0. */ -import { EuiAccordion, EuiSpacer, EuiTitle, useEuiTheme, useEuiFontSize } from '@elastic/eui'; +import { EuiAccordion, EuiSpacer, EuiTitle, useEuiFontSize, useEuiTheme } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React from 'react'; import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; -import * as i18n from './translations'; -import type { ObservedUserData } from './types'; -import { BasicTable } from '../../../../common/components/ml/tables/basic_table'; +import { EntityTable } from '../entity_table'; import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; -import { getObservedUserTableColumns } from './columns'; -import { ONE_WEEK_IN_HOURS } from './constants'; import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect'; -import { OBSERVED_USER_QUERY_ID } from '../../../../explore/users/containers/users/observed_details'; -import { useObservedUserItems } from './hooks/use_observed_user_items'; +import * as i18n from './translations'; +import type { EntityTableRows } from '../entity_table/types'; +import { ONE_WEEK_IN_HOURS } from '../constants'; +import type { ObservedEntityData } from './types'; -export const ObservedUser = ({ - observedUser, +export const ObservedEntity = ({ + observedData, contextID, scopeId, isDraggable, + observedFields, + queryId, }: { - observedUser: ObservedUserData; + observedData: ObservedEntityData; contextID: string; scopeId: string; isDraggable: boolean; + observedFields: EntityTableRows>; + queryId: string; }) => { const { euiTheme } = useEuiTheme(); - const observedItems = useObservedUserItems(observedUser); - - const observedUserTableColumns = useMemo( - () => getObservedUserTableColumns(contextID, scopeId, isDraggable), - [contextID, scopeId, isDraggable] - ); const xsFontSize = useEuiFontSize('xxs').fontSize; return ( @@ -45,11 +41,11 @@ export const ObservedUser = ({ - + - {observedUser.lastSeen.date && ( + {observedData.lastSeen.date && ( @@ -101,17 +94,12 @@ export const ObservedUser = ({ `} > - - diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/translations.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/translations.ts new file mode 100644 index 0000000000000..7fa12c6ef2789 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const OBSERVED_DATA_TITLE = i18n.translate( + 'xpack.securitySolution.flyout.entityDetails.observedDataTitle', + { + defaultMessage: 'Observed data', + } +); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/types.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/types.ts new file mode 100644 index 0000000000000..013dea6573956 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/types.ts @@ -0,0 +1,27 @@ +/* + * 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 { BasicEntityData } from '../entity_table/types'; +import type { AnomalyTableProviderChildrenProps } from '../../../../common/components/ml/anomaly/anomaly_table_provider'; + +export interface FirstLastSeenData { + date: string | null | undefined; + isLoading: boolean; +} + +export interface EntityAnomalies { + isLoading: AnomalyTableProviderChildrenProps['isLoadingAnomaliesData']; + anomalies: AnomalyTableProviderChildrenProps['anomaliesData']; + jobNameById: AnomalyTableProviderChildrenProps['jobNameById']; +} + +export interface ObservedEntityData extends BasicEntityData { + firstSeen: FirstLastSeenData; + lastSeen: FirstLastSeenData; + anomalies: EntityAnomalies; + details: T; +} diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx index ae96a76c68d4e..6d902886f4f37 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx @@ -9,11 +9,14 @@ import React, { useMemo } from 'react'; import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; import { useManagedUser } from '../../../timelines/components/side_panel/new_user_detail/hooks/use_managed_user'; -import { PanelHeader } from './header'; -import { PanelContent } from './content'; -import type { LeftPanelTabsType, UserDetailsLeftPanelTab } from './tabs'; import { useTabs } from './tabs'; import { FlyoutLoading } from '../../shared/components/flyout_loading'; +import type { + EntityDetailsLeftPanelTab, + LeftPanelTabsType, +} from '../shared/left_panel/left_panel_header'; +import { LeftPanelHeader } from '../shared/left_panel/left_panel_header'; +import { LeftPanelContent } from '../shared/left_panel/left_panel_content'; interface RiskInputsParam { alertIds: string[]; @@ -44,8 +47,12 @@ export const UserDetailsPanel = ({ riskInputs, user, path }: UserDetailsPanelPro return ( <> - - + + ); }; @@ -65,7 +72,7 @@ const useSelectedTab = ( return tabs.find((tab) => tab.id === path.tab)?.id ?? defaultTab; }, [path, tabs]); - const setSelectedTabId = (tabId: UserDetailsLeftPanelTab) => { + const setSelectedTabId = (tabId: EntityDetailsLeftPanelTab) => { openLeftPanel({ id: UserDetailsPanelKey, path: { diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx index 61f408a5c0ade..d6cfead911131 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -19,19 +18,8 @@ import type { import { ENTRA_TAB_TEST_ID, OKTA_TAB_TEST_ID } from './test_ids'; import { AssetDocumentTab } from './tabs/asset_document'; import { RightPanelProvider } from '../../document_details/right/context'; - -export type LeftPanelTabsType = Array<{ - id: UserDetailsLeftPanelTab; - 'data-test-subj': string; - name: ReactElement; - content: React.ReactElement; -}>; - -export enum UserDetailsLeftPanelTab { - RISK_INPUTS = 'risk_inputs', - OKTA = 'okta_document', - ENTRA = 'entra_document', -} +import type { LeftPanelTabsType } from '../shared/left_panel/left_panel_header'; +import { EntityDetailsLeftPanelTab } from '../shared/left_panel/left_panel_header'; export const useTabs = (managedUser: ManagedUserHits, alertIds: string[]): LeftPanelTabsType => useMemo(() => { @@ -55,7 +43,7 @@ export const useTabs = (managedUser: ManagedUserHits, alertIds: string[]): LeftP }, [alertIds, managedUser]); const getOktaTab = (oktaManagedUser: ManagedUserHit) => ({ - id: UserDetailsLeftPanelTab.OKTA, + id: EntityDetailsLeftPanelTab.OKTA, 'data-test-subj': OKTA_TAB_TEST_ID, name: ( ({ const getEntraTab = (entraManagedUser: ManagedUserHit) => { return { - id: UserDetailsLeftPanelTab.ENTRA, + id: EntityDetailsLeftPanelTab.ENTRA, 'data-test-subj': ENTRA_TAB_TEST_ID, name: ( ; managedUser: ManagedUserData; riskScoreState: RiskScoreState; contextID: string; scopeId: string; isDraggable: boolean; - openDetailsPanel: (tab: UserDetailsLeftPanelTab) => void; + openDetailsPanel: (tab: EntityDetailsLeftPanelTab) => void; } export const UserPanelContent = ({ @@ -40,6 +40,8 @@ export const UserPanelContent = ({ isDraggable, openDetailsPanel, }: UserPanelContentProps) => { + const observedFields = useObservedUserItems(observedUser); + return ( {riskScoreState.isModuleEnabled && riskScoreState.data?.length !== 0 && ( @@ -52,11 +54,13 @@ export const UserPanelContent = ({ )} - ; managedUser: ManagedUserData; } diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/translations.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/translations.ts new file mode 100644 index 0000000000000..7a3a6744e0c7b --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/translations.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const USER_ID = i18n.translate('xpack.securitySolution.timeline.userDetails.userIdLabel', { + defaultMessage: 'User ID', +}); + +export const MAX_ANOMALY_SCORE_BY_JOB = i18n.translate( + 'xpack.securitySolution.timeline.userDetails.maxAnomalyScoreByJobLabel', + { + defaultMessage: 'Max anomaly score by job', + } +); + +export const FIRST_SEEN = i18n.translate( + 'xpack.securitySolution.timeline.userDetails.firstSeenLabel', + { + defaultMessage: 'First seen', + } +); + +export const LAST_SEEN = i18n.translate( + 'xpack.securitySolution.timeline.userDetails.lastSeenLabel', + { + defaultMessage: 'Last seen', + } +); + +export const OPERATING_SYSTEM_TITLE = i18n.translate( + 'xpack.securitySolution.timeline.userDetails.hostOsNameLabel', + { + defaultMessage: 'Operating system', + } +); + +export const FAMILY = i18n.translate('xpack.securitySolution.timeline.userDetails.familyLabel', { + defaultMessage: 'Family', +}); + +export const IP_ADDRESSES = i18n.translate( + 'xpack.securitySolution.timeline.userDetails.ipAddressesLabel', + { + defaultMessage: 'IP addresses', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_observed_user.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user.ts similarity index 65% rename from x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_observed_user.ts rename to x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user.ts index d3d2c4fde90a1..26b04a7f48267 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_observed_user.ts +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user.ts @@ -6,28 +6,18 @@ */ import { useMemo } from 'react'; -import { useObservedUserDetails } from '../../../../../explore/users/containers/users/observed_details'; -import type { UserItem } from '../../../../../../common/search_strategy'; -import { Direction, NOT_EVENT_KIND_ASSET_FILTER } from '../../../../../../common/search_strategy'; -import { useSourcererDataView } from '../../../../../common/containers/sourcerer'; -import { useGlobalTime } from '../../../../../common/containers/use_global_time'; -import { useFirstLastSeen } from '../../../../../common/containers/use_first_last_seen'; -import { useQueryInspector } from '../../../../../common/components/page/manage_query'; +import { useQueryInspector } from '../../../../common/components/page/manage_query'; +import type { ObservedEntityData } from '../../shared/observed_entity/types'; +import { useObservedUserDetails } from '../../../../explore/users/containers/users/observed_details'; +import type { UserItem } from '../../../../../common/search_strategy'; +import { Direction, NOT_EVENT_KIND_ASSET_FILTER } from '../../../../../common/search_strategy'; +import { useSourcererDataView } from '../../../../common/containers/sourcerer'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { useFirstLastSeen } from '../../../../common/containers/use_first_last_seen'; -export interface ObserverUser { - details: UserItem; - isLoading: boolean; - firstSeen: { - date: string | null | undefined; - isLoading: boolean; - }; - lastSeen: { - date: string | null | undefined; - isLoading: boolean; - }; -} - -export const useObservedUser = (userName: string): ObserverUser => { +export const useObservedUser = ( + userName: string +): Omit, 'anomalies'> => { const { selectedPatterns } = useSourcererDataView(); const { to, from, isInitializing, deleteQuery, setQuery } = useGlobalTime(); @@ -68,7 +58,7 @@ export const useObservedUser = (userName: string): ObserverUser => { return useMemo( () => ({ details: observedUserDetails, - isLoading: loadingObservedUser, + isLoading: loadingObservedUser && loadingLastSeen && loadingFirstSeen, firstSeen: { date: firstSeen, isLoading: loadingFirstSeen, diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_observed_user_items.test.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.test.ts similarity index 93% rename from x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_observed_user_items.test.ts rename to x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.test.ts index 40fd3c6089039..7170a60d5213e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_observed_user_items.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.test.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { TestProviders } from '@kbn/timelines-plugin/public/mock'; import { renderHook } from '@testing-library/react-hooks'; -import { TestProviders } from '../../../../../common/mock'; -import { mockObservedUser } from '../__mocks__'; +import { mockObservedUser } from '../mocks'; import { useObservedUserItems } from './use_observed_user_items'; describe('useManagedUserItems', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.ts new file mode 100644 index 0000000000000..ace75d20880bb --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user_items.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import type { UserItem } from '../../../../../common/search_strategy'; +import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities'; +import { getAnomaliesFields } from '../../../../timelines/components/side_panel/common'; +import * as i18n from './translations'; +import type { ObservedEntityData } from '../../shared/observed_entity/types'; +import type { EntityTableRows } from '../../shared/entity_table/types'; + +const basicUserFields: EntityTableRows> = [ + { + label: i18n.USER_ID, + getValues: (userData: ObservedEntityData) => userData.details.user?.id, + field: 'user.id', + }, + { + label: 'Domain', + getValues: (userData: ObservedEntityData) => userData.details.user?.domain, + field: 'user.domain', + }, + { + label: i18n.FIRST_SEEN, + getValues: (userData: ObservedEntityData) => + userData.firstSeen.date ? [userData.firstSeen.date] : undefined, + field: '@timestamp', + }, + { + label: i18n.LAST_SEEN, + getValues: (userData: ObservedEntityData) => + userData.lastSeen.date ? [userData.lastSeen.date] : undefined, + field: '@timestamp', + }, + { + label: i18n.OPERATING_SYSTEM_TITLE, + getValues: (userData: ObservedEntityData) => userData.details.host?.os?.name, + field: 'host.os.name', + }, + { + label: i18n.FAMILY, + getValues: (userData: ObservedEntityData) => userData.details.host?.os?.family, + field: 'host.os.family', + }, + { + label: i18n.IP_ADDRESSES, + getValues: (userData: ObservedEntityData) => userData.details.host?.ip, + field: 'host.ip', + }, +]; + +export const useObservedUserItems = ( + userData: ObservedEntityData +): EntityTableRows> => { + const mlCapabilities = useMlCapabilities(); + + const fields: EntityTableRows> = useMemo( + () => [...basicUserFields, ...getAnomaliesFields(mlCapabilities)], + [mlCapabilities] + ); + + if (!userData.details) return []; + + return fields; +}; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx index 76168bc01c842..043be4f4da90e 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx @@ -11,7 +11,6 @@ import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; import { ManagedUserDatasetKey } from '../../../../common/search_strategy/security_solution/users/managed_details'; import { useManagedUser } from '../../../timelines/components/side_panel/new_user_detail/hooks/use_managed_user'; -import { useObservedUser } from '../../../timelines/components/side_panel/new_user_detail/hooks/use_observed_user'; import { useQueryInspector } from '../../../common/components/page/manage_query'; import { UsersType } from '../../../explore/users/store/model'; import { getCriteriaFromUsersType } from '../../../common/components/ml/criteria/get_criteria_from_users_type'; @@ -24,7 +23,8 @@ import { FlyoutNavigation } from '../../shared/components/flyout_navigation'; import { UserPanelContent } from './content'; import { UserPanelHeader } from './header'; import { UserDetailsPanelKey } from '../user_details_left'; -import type { UserDetailsLeftPanelTab } from '../user_details_left/tabs'; +import { useObservedUser } from './hooks/use_observed_user'; +import type { EntityDetailsLeftPanelTab } from '../shared/left_panel/left_panel_header'; export interface UserPanelProps extends Record { contextID: string; @@ -79,7 +79,7 @@ export const UserPanel = ({ contextID, scopeId, userName, isDraggable }: UserPan const { openLeftPanel } = useExpandableFlyoutContext(); const openPanelTab = useCallback( - (tab?: UserDetailsLeftPanelTab) => { + (tab?: EntityDetailsLeftPanelTab) => { openLeftPanel({ id: UserDetailsPanelKey, params: { diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/mocks/index.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/mocks/index.ts index 88ab3c10241cb..c734b7849226c 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/mocks/index.ts +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/mocks/index.ts @@ -5,47 +5,43 @@ * 2.0. */ -import type { RiskScoreState } from '../../../../entity_analytics/api/hooks/use_risk_score'; -import type { RiskScoreEntity, UserRiskScore } from '../../../../../common/search_strategy'; -import { RiskSeverity } from '../../../../../common/search_strategy'; -import { RiskCategories } from '../../../../../common/entity_analytics/risk_engine'; +import { mockAnomalies } from '../../../../common/components/ml/mock'; +import type { UserItem } from '../../../../../common/search_strategy'; +import type { ObservedEntityData } from '../../shared/observed_entity/types'; -const userRiskScore: UserRiskScore = { - '@timestamp': '626569200000', +const anomaly = mockAnomalies.anomalies[0]; + +const observedUserDetails = { user: { - name: 'test', - risk: { - rule_risks: [], - calculated_score_norm: 70, - multipliers: [], - calculated_level: RiskSeverity.high, - inputs: [ - { - id: '_id', - index: '_index', - category: RiskCategories.category_1, - description: 'Alert from Rule: My rule', - risk_score: 30, - timestamp: '2021-08-19T18:55:59.000Z', - }, - ], + id: ['1234', '321'], + domain: ['test domain', 'another test domain'], + }, + host: { + ip: ['10.0.0.1', '127.0.0.1'], + os: { + name: ['testOs'], + family: ['testFamily'], }, }, - alertsCount: 0, - oldestAlertTimestamp: '626569200000', }; -export const mockRiskScoreState: RiskScoreState = { - data: [userRiskScore], - inspect: { - dsl: [], - response: [], +export const mockObservedUser: ObservedEntityData = { + details: observedUserDetails, + isLoading: false, + firstSeen: { + isLoading: false, + date: '2023-02-23T20:03:17.489Z', + }, + lastSeen: { + isLoading: false, + date: '2023-02-23T20:03:17.489Z', + }, + anomalies: { + isLoading: false, + anomalies: { + anomalies: [anomaly], + interval: '', + }, + jobNameById: { [anomaly.jobId]: 'job_name' }, }, - isInspected: false, - refetch: () => {}, - totalCount: 0, - isModuleEnabled: true, - isAuthorized: true, - isDeprecated: false, - loading: false, }; diff --git a/x-pack/plugins/security_solution/public/flyout/index.tsx b/x-pack/plugins/security_solution/public/flyout/index.tsx index ef7e182324c63..c72417bd2004b 100644 --- a/x-pack/plugins/security_solution/public/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/index.tsx @@ -26,6 +26,11 @@ import type { UserPanelExpandableFlyoutProps } from './entity_details/user_right import { UserPanel, UserPanelKey } from './entity_details/user_right'; import type { UserDetailsPanelProps } from './entity_details/user_details_left'; import { UserDetailsPanel, UserDetailsPanelKey } from './entity_details/user_details_left'; +import type { HostPanelExpandableFlyoutProps } from './entity_details/host_right'; +import { HostPanel, HostPanelKey } from './entity_details/host_right'; +import type { HostDetailsExpandableFlyoutProps } from './entity_details/host_details_left'; +import { HostDetailsPanel, HostDetailsPanelKey } from './entity_details/host_details_left'; + /** * List of all panels that will be used within the document details expandable flyout. * This needs to be passed to the expandable flyout registeredPanels property. @@ -73,6 +78,16 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels'] ), }, + { + key: HostPanelKey, + component: (props) => , + }, + { + key: HostDetailsPanelKey, + component: (props) => ( + + ), + }, ]; export const SecuritySolutionFlyout = memo(() => ( diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index a6409c587e0a6..9054605250808 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -10,7 +10,6 @@ import { euiDarkVars as darkTheme, euiLightVars as lightTheme } from '@kbn/ui-th import { getOr } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; import type { HostItem } from '../../../../common/search_strategy'; import { buildHostNamesFilter, RiskScoreEntity } from '../../../../common/search_strategy'; import { DEFAULT_DARK_MODE } from '../../../../common/constants'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/common.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/common.tsx new file mode 100644 index 0000000000000..5601ef2dec99d --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/common.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import type { ObservedEntityData } from '../../../flyout/entity_details/shared/observed_entity/types'; +import type { MlCapabilitiesProvider } from '../../../common/components/ml/permissions/ml_capabilities_provider'; +import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import type { HostItem } from '../../../../common/search_strategy'; +import { AnomaliesField } from './new_user_detail/anomalies_field'; + +export const getAnomaliesFields = (mlCapabilities: MlCapabilitiesProvider) => [ + { + label: i18n.translate('xpack.securitySolution.timeline.sidePanel.maxAnomalyScoreByJobTitle', { + defaultMessage: 'Max anomaly score by job', + }), + render: (hostData: ObservedEntityData) => + hostData.anomalies ? : getEmptyTagValue(), + isVisible: () => hasMlUserPermissions(mlCapabilities), + }, +]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_host_detail/__mocks__/index.ts b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_host_detail/__mocks__/index.ts new file mode 100644 index 0000000000000..1d2c862b00a92 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_host_detail/__mocks__/index.ts @@ -0,0 +1,69 @@ +/* + * 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 { ObservedEntityData } from '../../../../../flyout/entity_details/shared/observed_entity/types'; +import type { HostItem } from '../../../../../../common/search_strategy'; +import { RiskSeverity } from '../../../../../../common/search_strategy'; + +const hostRiskScore = { + '@timestamp': '123456', + host: { + name: 'test', + risk: { + rule_risks: [], + calculated_score_norm: 70, + multipliers: [], + calculated_level: RiskSeverity.high, + }, + }, + alertsCount: 0, + oldestAlertTimestamp: '123456', +}; + +export const mockRiskScoreState = { + data: [hostRiskScore], + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + refetch: () => {}, + totalCount: 0, + isModuleEnabled: true, + isAuthorized: true, + isDeprecated: false, + loading: false, +}; + +export const mockObservedHost = { + // ... + // user: { + // id: ['1234', '321'], + // domain: ['test domain', 'another test domain'], + // }, + // host: { + // ip: ['10.0.0.1', '127.0.0.1'], + // os: { + // name: ['testOs'], + // family: ['testFamily'], + // }, + // }, +}; + +export const mockObservedHostData: ObservedEntityData = { + details: mockObservedHost, + isLoading: false, + firstSeen: { + isLoading: false, + date: '2023-02-23T20:03:17.489Z', + }, + lastSeen: { + isLoading: false, + date: '2023-02-23T20:03:17.489Z', + }, + anomalies: { isLoading: false, anomalies: null, jobNameById: {} }, +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_host_detail/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_host_detail/translations.ts new file mode 100644 index 0000000000000..7486b9dcd0973 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_host_detail/translations.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +// TODO delete unused translations + +export const OBSERVED_BADGE = i18n.translate( + 'xpack.securitySolution.timeline.userDetails.observedBadge', + { + defaultMessage: 'OBSERVED', + } +); + +export const MANAGED_BADGE = i18n.translate( + 'xpack.securitySolution.timeline.userDetails.managedBadge', + { + defaultMessage: 'MANAGED', + } +); + +export const MANAGED_DATA_TITLE = i18n.translate( + 'xpack.securitySolution.timeline.userDetails.managedDataTitle', + { + defaultMessage: 'Managed data', + } +); + +export const RISK_SCORE = i18n.translate( + 'xpack.securitySolution.timeline.userDetails.riskScoreLabel', + { + defaultMessage: 'Risk score', + } +); + +export const VALUES_COLUMN_TITLE = i18n.translate( + 'xpack.securitySolution.timeline.userDetails.valuesColumnTitle', + { + defaultMessage: 'Values', + } +); + +export const FIELD_COLUMN_TITLE = i18n.translate( + 'xpack.securitySolution.timeline.userDetails.fieldColumnTitle', + { + defaultMessage: 'Field', + } +); + +export const CLOSE_BUTTON = i18n.translate( + 'xpack.securitySolution.timeline.userDetails.closeButton', + { + defaultMessage: 'close', + } +); + +export const OBSERVED_USER_INSPECT_TITLE = i18n.translate( + 'xpack.securitySolution.timeline.userDetails.observedUserInspectTitle', + { + defaultMessage: 'Observed user', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/__mocks__/index.ts b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/__mocks__/index.ts index 65c6bd974b83a..43abce1104467 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/__mocks__/index.ts @@ -11,8 +11,7 @@ import type { } from '../../../../../../common/search_strategy/security_solution/users/managed_details'; import { ManagedUserDatasetKey } from '../../../../../../common/search_strategy/security_solution/users/managed_details'; import { RiskSeverity } from '../../../../../../common/search_strategy'; -import { mockAnomalies } from '../../../../../common/components/ml/mock'; -import type { ManagedUserData, ObservedUserData } from '../types'; +import type { ManagedUserData } from '../types'; const userRiskScore = { '@timestamp': '123456', @@ -44,43 +43,6 @@ export const mockRiskScoreState = { loading: false, }; -const anomaly = mockAnomalies.anomalies[0]; - -export const observedUserDetails = { - user: { - id: ['1234', '321'], - domain: ['test domain', 'another test domain'], - }, - host: { - ip: ['10.0.0.1', '127.0.0.1'], - os: { - name: ['testOs'], - family: ['testFamily'], - }, - }, -}; - -export const mockObservedUser: ObservedUserData = { - details: observedUserDetails, - isLoading: false, - firstSeen: { - isLoading: false, - date: '2023-02-23T20:03:17.489Z', - }, - lastSeen: { - isLoading: false, - date: '2023-02-23T20:03:17.489Z', - }, - anomalies: { - isLoading: false, - anomalies: { - anomalies: [anomaly], - interval: '', - }, - jobNameById: { [anomaly.jobId]: 'job_name' }, - }, -}; - export const mockOktaUserFields: ManagedUserFields = { '@timestamp': ['2023-11-16T13:42:23.074Z'], 'event.dataset': [ManagedUserDatasetKey.OKTA], diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/anomalies_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/anomalies_field.tsx new file mode 100644 index 0000000000000..8a53c62e22bad --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/anomalies_field.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { AnomalyScores } from '../../../../common/components/ml/score/anomaly_scores'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { scoreIntervalToDateTime } from '../../../../common/components/ml/score/score_interval_to_datetime'; +import { InputsModelId } from '../../../../common/store/inputs/constants'; +import { setAbsoluteRangeDatePicker } from '../../../../common/store/inputs/actions'; +import type { UserAnomalies } from './types'; + +export const AnomaliesField = ({ anomalies }: { anomalies: UserAnomalies }) => { + const { to, from } = useGlobalTime(); + const dispatch = useDispatch(); + + const narrowDateRange = useCallback( + (score, interval) => { + const fromTo = scoreIntervalToDateTime(score, interval); + dispatch( + setAbsoluteRangeDatePicker({ + id: InputsModelId.global, + from: fromTo.from, + to: fromTo.to, + }) + ); + }, + [dispatch] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/columns.tsx index 8c4f31ea12141..da4e82976d515 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/columns.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/columns.tsx @@ -6,31 +6,16 @@ */ import { css } from '@emotion/react'; -import React, { useCallback } from 'react'; -import { head } from 'lodash/fp'; +import React from 'react'; import { euiLightVars } from '@kbn/ui-theme'; import type { EuiBasicTableColumn } from '@elastic/eui'; -import { useDispatch } from 'react-redux'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { DefaultFieldRenderer } from '../../field_renderers/field_renderers'; -import type { - ManagedUsersTableColumns, - ManagedUserTable, - ObservedUsersTableColumns, - ObservedUserTable, - UserAnomalies, -} from './types'; +import type { ManagedUsersTableColumns, ManagedUserTable } from './types'; import * as i18n from './translations'; import { defaultToEmptyTag } from '../../../../common/components/empty_value'; -import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; -import { AnomalyScores } from '../../../../common/components/ml/score/anomaly_scores'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { scoreIntervalToDateTime } from '../../../../common/components/ml/score/score_interval_to_datetime'; -import { InputsModelId } from '../../../../common/store/inputs/constants'; -import { setAbsoluteRangeDatePicker } from '../../../../common/store/inputs/actions'; -import { getSourcererScopeId } from '../../../../helpers'; -const fieldColumn: EuiBasicTableColumn = { +const fieldColumn: EuiBasicTableColumn = { name: i18n.FIELD_COLUMN_TITLE, field: 'label', render: (label: string, { field }) => ( @@ -68,71 +53,3 @@ export const getManagedUserTableColumns = ( }, }, ]; - -function isAnomalies( - field: string | undefined, - values: UserAnomalies | unknown -): values is UserAnomalies { - return field === 'anomalies'; -} - -export const getObservedUserTableColumns = ( - contextID: string, - scopeId: string, - isDraggable: boolean -): ObservedUsersTableColumns => [ - fieldColumn, - { - name: i18n.VALUES_COLUMN_TITLE, - field: 'values', - render: (values: ObservedUserTable['values'], { field }) => { - if (isAnomalies(field, values) && values) { - return ; - } - - if (field === '@timestamp') { - return ; - } - - return ( - - ); - }, - }, -]; - -const AnomaliesField = ({ anomalies }: { anomalies: UserAnomalies }) => { - const { to, from } = useGlobalTime(); - const dispatch = useDispatch(); - - const narrowDateRange = useCallback( - (score, interval) => { - const fromTo = scoreIntervalToDateTime(score, interval); - dispatch( - setAbsoluteRangeDatePicker({ - id: InputsModelId.global, - from: fromTo.from, - to: fromTo.to, - }) - ); - }, - [dispatch] - ); - - return ( - - ); -}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_observed_user_items.ts b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_observed_user_items.ts deleted file mode 100644 index d6390b210d586..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/hooks/use_observed_user_items.ts +++ /dev/null @@ -1,48 +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 } from 'react'; -import * as i18n from '../translations'; -import type { ObservedUserData, ObservedUserTable } from '../types'; - -export const useObservedUserItems = (userData: ObservedUserData): ObservedUserTable[] => - useMemo( - () => - !userData.details - ? [] - : [ - { label: i18n.USER_ID, values: userData.details.user?.id, field: 'user.id' }, - { label: 'Domain', values: userData.details.user?.domain, field: 'user.domain' }, - { - label: i18n.MAX_ANOMALY_SCORE_BY_JOB, - field: 'anomalies', - values: userData.anomalies, - }, - { - label: i18n.FIRST_SEEN, - values: userData.firstSeen.date ? [userData.firstSeen.date] : undefined, - field: '@timestamp', - }, - { - label: i18n.LAST_SEEN, - values: userData.lastSeen.date ? [userData.lastSeen.date] : undefined, - field: '@timestamp', - }, - { - label: i18n.OPERATING_SYSTEM_TITLE, - values: userData.details.host?.os?.name, - field: 'host.os.name', - }, - { - label: i18n.FAMILY, - values: userData.details.host?.os?.family, - field: 'host.os.family', - }, - { label: i18n.IP_ADDRESSES, values: userData.details.host?.ip, field: 'host.ip' }, - ], - [userData.details, userData.anomalies, userData.firstSeen, userData.lastSeen] - ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/risk_score_field.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/risk_score_field.test.tsx deleted file mode 100644 index 48d927c97030c..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/risk_score_field.test.tsx +++ /dev/null @@ -1,46 +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 { render } from '@testing-library/react'; -import React from 'react'; -import { TestProviders } from '../../../../common/mock'; -import { RiskScoreField } from './risk_score_field'; -import { mockRiskScoreState } from './__mocks__'; -import { getEmptyValue } from '../../../../common/components/empty_value'; - -describe('RiskScoreField', () => { - it('renders', () => { - const { getByTestId } = render( - - - - ); - - expect(getByTestId('user-details-risk-score')).toBeInTheDocument(); - expect(getByTestId('user-details-risk-score')).toHaveTextContent('70'); - }); - - it('does not render content when the license is invalid', () => { - const { queryByTestId } = render( - - - - ); - - expect(queryByTestId('user-details-risk-score')).not.toBeInTheDocument(); - }); - - it('renders empty tag when risk score is undefined', () => { - const { getByTestId } = render( - - - - ); - - expect(getByTestId('user-details-risk-score')).toHaveTextContent(getEmptyValue()); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/risk_score_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/risk_score_field.tsx deleted file mode 100644 index fab77b92582f6..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/risk_score_field.tsx +++ /dev/null @@ -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 { EuiFlexItem, EuiFlexGroup, useEuiFontSize, useEuiTheme } from '@elastic/eui'; - -import React from 'react'; -import { css } from '@emotion/react'; - -import styled from 'styled-components'; -import * as i18n from './translations'; - -import { RiskScoreEntity } from '../../../../../common/search_strategy'; -import { getEmptyTagValue } from '../../../../common/components/empty_value'; -import { RiskScoreLevel } from '../../../../entity_analytics/components/severity/common'; -import type { RiskScoreState } from '../../../../entity_analytics/api/hooks/use_risk_score'; -import { RiskScoreDocTooltip } from '../../../../overview/components/common'; - -export const TooltipContainer = styled.div` - padding: ${({ theme }) => theme.eui.euiSizeS}; -`; - -export const RiskScoreField = ({ - riskScoreState, -}: { - riskScoreState: RiskScoreState; -}) => { - const { euiTheme } = useEuiTheme(); - const { fontSize: xsFontSize } = useEuiFontSize('xs'); - const { data: userRisk, isAuthorized: isRiskScoreAuthorized } = riskScoreState; - const userRiskData = userRisk && userRisk.length > 0 ? userRisk[0] : undefined; - - if (!isRiskScoreAuthorized) { - return null; - } - - return ( - - - - {i18n.RISK_SCORE} - {': '} - - - {userRiskData ? ( - - - {Math.round(userRiskData.user.risk.calculated_score_norm)} - - - - - - - - - ) : ( - getEmptyTagValue() - )} - - ); -}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/translations.ts index ce5e34ce3249b..ebeb5d26cf362 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/translations.ts @@ -81,49 +81,6 @@ export const FIELD_COLUMN_TITLE = i18n.translate( } ); -export const USER_ID = i18n.translate('xpack.securitySolution.timeline.userDetails.userIdLabel', { - defaultMessage: 'User ID', -}); - -export const MAX_ANOMALY_SCORE_BY_JOB = i18n.translate( - 'xpack.securitySolution.timeline.userDetails.maxAnomalyScoreByJobLabel', - { - defaultMessage: 'Max anomaly score by job', - } -); - -export const FIRST_SEEN = i18n.translate( - 'xpack.securitySolution.timeline.userDetails.firstSeenLabel', - { - defaultMessage: 'First seen', - } -); - -export const LAST_SEEN = i18n.translate( - 'xpack.securitySolution.timeline.userDetails.lastSeenLabel', - { - defaultMessage: 'Last seen', - } -); - -export const OPERATING_SYSTEM_TITLE = i18n.translate( - 'xpack.securitySolution.timeline.userDetails.hostOsNameLabel', - { - defaultMessage: 'Operating system', - } -); - -export const FAMILY = i18n.translate('xpack.securitySolution.timeline.userDetails.familyLabel', { - defaultMessage: 'Family', -}); - -export const IP_ADDRESSES = i18n.translate( - 'xpack.securitySolution.timeline.userDetails.ipAddressesLabel', - { - defaultMessage: 'IP addresses', - } -); - export const NO_ACTIVE_INTEGRATION_TITLE = i18n.translate( 'xpack.securitySolution.timeline.userDetails.noActiveIntegrationTitle', { @@ -168,13 +125,6 @@ export const CLOSE_BUTTON = i18n.translate( } ); -export const OBSERVED_USER_INSPECT_TITLE = i18n.translate( - 'xpack.securitySolution.timeline.userDetails.observedUserInspectTitle', - { - defaultMessage: 'Observed user', - } -); - export const MANAGED_USER_INSPECT_TITLE = i18n.translate( 'xpack.securitySolution.timeline.userDetails.managedUserInspectTitle', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/types.ts b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/types.ts index edefb6ac75100..721ba17370709 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/types.ts @@ -7,44 +7,17 @@ import type { EuiBasicTableColumn } from '@elastic/eui'; import type { SearchTypes } from '../../../../../common/detection_engine/types'; -import type { UserItem } from '../../../../../common/search_strategy'; import type { ManagedUserHits } from '../../../../../common/search_strategy/security_solution/users/managed_details'; -import type { AnomalyTableProviderChildrenProps } from '../../../../common/components/ml/anomaly/anomaly_table_provider'; - -export interface ObservedUserTable { - values: string[] | null | undefined | UserAnomalies; - field: string; -} export interface ManagedUserTable { value: SearchTypes[]; field?: string; } -export type ObservedUsersTableColumns = Array>; export type ManagedUsersTableColumns = Array>; -export interface ObservedUserData { - isLoading: boolean; - details: UserItem; - firstSeen: FirstLastSeenData; - lastSeen: FirstLastSeenData; - anomalies: UserAnomalies; -} - export interface ManagedUserData { isLoading: boolean; data: ManagedUserHits | undefined; isIntegrationEnabled: boolean; } - -export interface FirstLastSeenData { - date: string | null | undefined; - isLoading: boolean; -} - -export interface UserAnomalies { - isLoading: AnomalyTableProviderChildrenProps['isLoadingAnomaliesData']; - anomalies: AnomalyTableProviderChildrenProps['anomaliesData']; - jobNameById: AnomalyTableProviderChildrenProps['jobNameById']; -} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx index 37503f7b905ec..ac46f97ad2793 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx @@ -9,9 +9,13 @@ import React, { useCallback, useContext, useMemo } from 'react'; import type { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import { isString } from 'lodash/fp'; +import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { TableId } from '@kbn/securitysolution-data-table'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { HostPanelKey } from '../../../../../flyout/entity_details/host_right'; import type { ExpandedDetailType } from '../../../../../../common/types'; import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context'; -import { getScopedActions } from '../../../../../helpers'; +import { getScopedActions, isTimelineScope } from '../../../../../helpers'; import { HostDetailsLink } from '../../../../../common/components/links'; import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; import { DefaultDraggable } from '../../../../../common/components/draggables'; @@ -46,6 +50,9 @@ const HostNameComponent: React.FC = ({ title, value, }) => { + const isNewHostDetailsFlyoutEnable = useIsExperimentalFeatureEnabled('newHostDetailsFlyout'); + const { openRightPanel } = useExpandableFlyoutContext(); + const dispatch = useDispatch(); const eventContext = useContext(StatefulEventContext); const hostName = `${value}`; @@ -60,29 +67,52 @@ const HostNameComponent: React.FC = ({ } if (eventContext && isInTimelineContext) { const { timelineID, tabType } = eventContext; - const updatedExpandedDetail: ExpandedDetailType = { - panelView: 'hostDetail', - params: { - hostName, - }, - }; - const scopedActions = getScopedActions(timelineID); - if (scopedActions) { - dispatch( - scopedActions.toggleDetailPanel({ - ...updatedExpandedDetail, - id: timelineID, - tabType: tabType as TimelineTabs, - }) - ); - } - if (timelineID === TimelineId.active && tabType === TimelineTabs.query) { - activeTimeline.toggleExpandedDetail({ ...updatedExpandedDetail }); + if (isNewHostDetailsFlyoutEnable && !isTimelineScope(timelineID)) { + openRightPanel({ + id: HostPanelKey, + params: { + hostName, + contextID: contextId, + scopeId: TableId.alertsOnAlertsPage, + isDraggable, + }, + }); + } else { + const updatedExpandedDetail: ExpandedDetailType = { + panelView: 'hostDetail', + params: { + hostName, + }, + }; + const scopedActions = getScopedActions(timelineID); + if (scopedActions) { + dispatch( + scopedActions.toggleDetailPanel({ + ...updatedExpandedDetail, + id: timelineID, + tabType: tabType as TimelineTabs, + }) + ); + } + + if (timelineID === TimelineId.active && tabType === TimelineTabs.query) { + activeTimeline.toggleExpandedDetail({ ...updatedExpandedDetail }); + } } } }, - [onClick, eventContext, isInTimelineContext, hostName, dispatch] + [ + onClick, + eventContext, + isInTimelineContext, + isNewHostDetailsFlyoutEnable, + openRightPanel, + hostName, + contextId, + isDraggable, + dispatch, + ] ); // The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined