From 32a45ba742c68cf7bda98abbb411b9d8a2923daa Mon Sep 17 00:00:00 2001 From: machadoum Date: Mon, 18 Dec 2023 16:14:20 +0100 Subject: [PATCH 01/13] 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 From 5d29c5d2caea39b7c4c2e23354ccb729b7ad9bf1 Mon Sep 17 00:00:00 2001 From: machadoum Date: Fri, 22 Dec 2023 09:33:25 +0100 Subject: [PATCH 02/13] Fix tests --- .../entity_details_flyout/index.tsx | 2 +- .../host_details_left/index.tsx | 7 +- .../entity_details/host_right/content.tsx | 4 +- .../host_right/fields/basic_host_fields.tsx | 4 +- .../host_right/fields/cloud_fields.ts | 4 +- .../fields/endpoint_policy_fields.tsx | 4 +- .../host_right/fields/translations.ts | 82 +++++---- .../entity_details/host_right/header.tsx | 2 +- .../host_right/hooks/use_observed_host.ts | 2 +- .../hooks/use_observed_host_fields.test.ts | 160 ++++++++++++++---- .../hooks/use_observed_host_fields.ts | 6 +- .../entity_details/host_right/index.test.tsx | 70 +++----- .../entity_details/host_right/index.tsx | 10 +- .../flyout/entity_details/mocks/index.ts | 138 ++++++++++++++- .../entity_details/shared/common.test.tsx | 45 +++++ .../entity_details/shared}/common.tsx | 4 +- .../components/anomalies_field.test.tsx | 44 +++++ .../shared/components}/anomalies_field.tsx | 4 +- .../{ => components}/entity_table/columns.tsx | 22 ++- .../{ => components}/entity_table/index.tsx | 2 +- .../{ => components}/entity_table/types.ts | 2 +- .../left_panel/left_panel_content.tsx | 2 +- .../left_panel/left_panel_header.tsx | 2 +- .../observed_entity/index.test.tsx | 18 +- .../observed_entity/index.tsx | 24 ++- .../{ => components}/observed_entity/types.ts | 2 +- .../shared/observed_entity/translations.ts | 15 -- .../user_details_left/index.tsx | 6 +- .../entity_details/user_details_left/tabs.tsx | 4 +- .../entity_details/user_right/content.tsx | 6 +- .../entity_details/user_right/header.tsx | 2 +- .../user_right/hooks/translations.ts | 21 ++- .../user_right/hooks/use_observed_user.ts | 2 +- .../hooks/use_observed_user_items.test.ts | 41 +++-- .../hooks/use_observed_user_items.ts | 6 +- .../entity_details/user_right/index.test.tsx | 13 +- .../entity_details/user_right/index.tsx | 2 +- .../entity_details/user_right/mocks/index.ts | 2 +- .../new_host_detail/__mocks__/index.ts | 69 -------- .../new_host_detail/translations.ts | 66 -------- 40 files changed, 550 insertions(+), 371 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/shared/common.test.tsx rename x-pack/plugins/security_solution/public/{timelines/components/side_panel => flyout/entity_details/shared}/common.tsx (86%) create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/anomalies_field.test.tsx rename x-pack/plugins/security_solution/public/{timelines/components/side_panel/new_user_detail => flyout/entity_details/shared/components}/anomalies_field.tsx (90%) rename x-pack/plugins/security_solution/public/flyout/entity_details/shared/{ => components}/entity_table/columns.tsx (70%) rename x-pack/plugins/security_solution/public/flyout/entity_details/shared/{ => components}/entity_table/index.tsx (93%) rename x-pack/plugins/security_solution/public/flyout/entity_details/shared/{ => components}/entity_table/types.ts (96%) rename x-pack/plugins/security_solution/public/flyout/entity_details/shared/{ => components}/left_panel/left_panel_content.tsx (94%) rename x-pack/plugins/security_solution/public/flyout/entity_details/shared/{ => components}/left_panel/left_panel_header.tsx (96%) rename x-pack/plugins/security_solution/public/flyout/entity_details/shared/{ => components}/observed_entity/index.test.tsx (62%) rename x-pack/plugins/security_solution/public/flyout/entity_details/shared/{ => components}/observed_entity/index.tsx (80%) rename x-pack/plugins/security_solution/public/flyout/entity_details/shared/{ => components}/observed_entity/types.ts (93%) delete mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/new_host_detail/__mocks__/index.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/new_host_detail/translations.ts 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 d4966db527879..64c79bfdb77f7 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,7 +7,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared/left_panel/left_panel_header'; +import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; import { PREFIX } from '../../../flyout/shared/test_ids'; import { RiskInputsTab } from './tabs/risk_inputs'; 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 index dcc15768309cb..3214dec23bdd6 100644 --- 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 @@ -8,8 +8,11 @@ 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'; +import { LeftPanelContent } from '../shared/components/left_panel/left_panel_content'; +import { + EntityDetailsLeftPanelTab, + LeftPanelHeader, +} from '../shared/components/left_panel/left_panel_header'; interface RiskInputsParam { alertIds: string[]; 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 index 7de3df63dc1ba..4a5ddcb3f181f 100644 --- 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 @@ -11,9 +11,9 @@ 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 { ObservedEntity } from '../shared/components/observed_entity'; import { HOST_PANEL_OBSERVED_HOST_QUERY_ID } from '.'; -import type { ObservedEntityData } from '../shared/observed_entity/types'; +import type { ObservedEntityData } from '../shared/components/observed_entity/types'; import { useObservedHostFields } from './hooks/use_observed_host_fields'; interface HostPanelContentProps { 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 index d34daa8243d1d..1229a08e7c956 100644 --- 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 @@ -12,8 +12,8 @@ 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'; +import type { ObservedEntityData } from '../../shared/components/observed_entity/types'; +import type { EntityTableRows } from '../../shared/components/entity_table/types'; export const basicHostFields: EntityTableRows> = [ { 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 index 435fa3fd53dcf..c4ea144a7db06 100644 --- 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 @@ -6,8 +6,8 @@ */ import type { HostItem } from '../../../../../common/search_strategy'; -import type { EntityTableRows } from '../../shared/entity_table/types'; -import type { ObservedEntityData } from '../../shared/observed_entity/types'; +import type { EntityTableRows } from '../../shared/components/entity_table/types'; +import type { ObservedEntityData } from '../../shared/components/observed_entity/types'; import * as i18n from './translations'; export const cloudFields: EntityTableRows> = [ 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 index b1a7ac9dc8f29..125534c785dd7 100644 --- 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 @@ -8,8 +8,8 @@ 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 type { EntityTableRows } from '../../shared/components/entity_table/types'; +import type { ObservedEntityData } from '../../shared/components/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'; 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 index 63ac4faf9c401..f4d64e7246a90 100644 --- 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 @@ -7,120 +7,138 @@ import { i18n } from '@kbn/i18n'; -export const HOST_ID = i18n.translate('xpack.securitySolution.host.details.overview.hostIdTitle', { - defaultMessage: 'Host ID', -}); +export const HOST_ID = i18n.translate( + 'xpack.securitySolution.flyout.entityDetails.host.hostIdTitle', + { + defaultMessage: 'Host ID', + } +); -export const FIRST_SEEN = i18n.translate('xpack.securitySolution.host.details.firstSeenTitle', { - defaultMessage: 'First seen', -}); +export const FIRST_SEEN = i18n.translate( + 'xpack.securitySolution.flyout.entityDetails.host.firstSeenTitle', + { + defaultMessage: 'First seen', + } +); -export const LAST_SEEN = i18n.translate('xpack.securitySolution.host.details.lastSeenTitle', { - defaultMessage: 'Last seen', -}); +export const LAST_SEEN = i18n.translate( + 'xpack.securitySolution.flyout.entityDetails.host.lastSeenTitle', + { + defaultMessage: 'Last seen', + } +); export const HOST_RISK_SCORE = i18n.translate( - 'xpack.securitySolution.host.details.overview.hostRiskScoreTitle', + 'xpack.securitySolution.flyout.entityDetails.host.hostRiskScoreTitle', { defaultMessage: 'Host risk score', } ); export const HOST_RISK_LEVEL = i18n.translate( - 'xpack.securitySolution.host.details.overview.hostRiskLevel', + 'xpack.securitySolution.flyout.entityDetails.host.hostRiskLevel', { defaultMessage: 'Host risk level', } ); export const IP_ADDRESSES = i18n.translate( - 'xpack.securitySolution.host.details.overview.ipAddressesTitle', + 'xpack.securitySolution.flyout.entityDetails.host.ipAddressesTitle', { defaultMessage: 'IP addresses', } ); export const MAC_ADDRESSES = i18n.translate( - 'xpack.securitySolution.host.details.overview.macAddressesTitle', + 'xpack.securitySolution.flyout.entityDetails.host.macAddressesTitle', { defaultMessage: 'MAC addresses', } ); export const PLATFORM = i18n.translate( - 'xpack.securitySolution.host.details.overview.platformTitle', + 'xpack.securitySolution.flyout.entityDetails.host.platformTitle', { defaultMessage: 'Platform', } ); -export const OS = i18n.translate('xpack.securitySolution.host.details.overview.osTitle', { +export const OS = i18n.translate('xpack.securitySolution.flyout.entityDetails.host.osTitle', { defaultMessage: 'Operating system', }); -export const FAMILY = i18n.translate('xpack.securitySolution.host.details.overview.familyTitle', { - defaultMessage: 'Family', -}); +export const FAMILY = i18n.translate( + 'xpack.securitySolution.flyout.entityDetails.host.familyTitle', + { + defaultMessage: 'Family', + } +); -export const VERSION = i18n.translate('xpack.securitySolution.host.details.versionLabel', { - defaultMessage: 'Version', -}); +export const VERSION = i18n.translate( + 'xpack.securitySolution.flyout.entityDetails.host.versionLabel', + { + defaultMessage: 'Version', + } +); export const ARCHITECTURE = i18n.translate( - 'xpack.securitySolution.host.details.architectureLabel', + 'xpack.securitySolution.flyout.entityDetails.host.architectureLabel', { defaultMessage: 'Architecture', } ); export const CLOUD_PROVIDER = i18n.translate( - 'xpack.securitySolution.host.details.overview.cloudProviderTitle', + 'xpack.securitySolution.flyout.entityDetails.host.cloudProviderTitle', { defaultMessage: 'Cloud provider', } ); -export const REGION = i18n.translate('xpack.securitySolution.host.details.overview.regionTitle', { - defaultMessage: 'Region', -}); +export const REGION = i18n.translate( + 'xpack.securitySolution.flyout.entityDetails.host.regionTitle', + { + defaultMessage: 'Region', + } +); export const INSTANCE_ID = i18n.translate( - 'xpack.securitySolution.host.details.overview.instanceIdTitle', + 'xpack.securitySolution.flyout.entityDetails.host.instanceIdTitle', { defaultMessage: 'Instance ID', } ); export const MACHINE_TYPE = i18n.translate( - 'xpack.securitySolution.host.details.overview.machineTypeTitle', + 'xpack.securitySolution.flyout.entityDetails.host.machineTypeTitle', { defaultMessage: 'Machine type', } ); export const ENDPOINT_POLICY = i18n.translate( - 'xpack.securitySolution.host.details.endpoint.endpointPolicy', + 'xpack.securitySolution.flyout.entityDetails.host.endpoint.endpointPolicy', { defaultMessage: 'Endpoint integration policy', } ); export const POLICY_STATUS = i18n.translate( - 'xpack.securitySolution.host.details.endpoint.policyStatus', + 'xpack.securitySolution.flyout.entityDetails.host.endpoint.policyStatus', { defaultMessage: 'Policy Status', } ); export const SENSORVERSION = i18n.translate( - 'xpack.securitySolution.host.details.endpoint.sensorversion', + 'xpack.securitySolution.flyout.entityDetails.host.endpoint.sensorversion', { defaultMessage: 'Endpoint version', } ); export const FLEET_AGENT_STATUS = i18n.translate( - 'xpack.securitySolution.host.details.endpoint.fleetAgentStatus', + 'xpack.securitySolution.flyout.entityDetails.host.endpoint.fleetAgentStatus', { defaultMessage: 'Agent status', } 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 index 2d2b32c6323d4..e8785a92acb6d 100644 --- 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 @@ -15,7 +15,7 @@ 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'; +import type { ObservedEntityData } from '../shared/components/observed_entity/types'; interface HostPanelHeaderProps { hostName: string; 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 index eb7e467b1d9da..f424fcfe44bb9 100644 --- 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 @@ -14,7 +14,7 @@ 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'; +import type { ObservedEntityData } from '../../shared/components/observed_entity/types'; export const useObservedHost = ( hostName: string 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 index 3adcafabbabc3..ea37bf40bfeef 100644 --- 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 @@ -5,10 +5,10 @@ * 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'; +import { mockObservedHostData } from '../../mocks'; describe('useManagedUserItems', () => { it('returns managed user items for Entra user', () => { @@ -16,42 +16,128 @@ describe('useManagedUserItems', () => { 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'], - }, + expect(result.current).toMatchInlineSnapshot(` + Array [ + Object { + "field": "host.id", + "getValues": [Function], + "label": "Host ID", + }, + Object { + "label": "First seen", + "render": [Function], + }, + Object { + "label": "Last seen", + "render": [Function], + }, + Object { + "field": "host.ip", + "getValues": [Function], + "label": "IP addresses", + "renderField": [Function], + }, + Object { + "field": "host.mac", + "getValues": [Function], + "label": "MAC addresses", + }, + Object { + "field": "host.os.platform", + "getValues": [Function], + "label": "Platform", + }, + Object { + "field": "host.os.name", + "getValues": [Function], + "label": "Operating system", + }, + Object { + "field": "host.os.family", + "getValues": [Function], + "label": "Family", + }, + Object { + "field": "host.os.version", + "getValues": [Function], + "label": "Version", + }, + Object { + "field": "host.architecture", + "getValues": [Function], + "label": "Architecture", + }, + Object { + "isVisible": [Function], + "label": "Max anomaly score by job", + "render": [Function], + }, + Object { + "field": "cloud.provider", + "getValues": [Function], + "label": "Cloud provider", + }, + Object { + "field": "cloud.region", + "getValues": [Function], + "label": "Region", + }, + Object { + "field": "cloud.instance.id", + "getValues": [Function], + "label": "Instance ID", + }, + Object { + "field": "cloud.machine.type", + "getValues": [Function], + "label": "Machine type", + }, + Object { + "isVisible": [Function], + "label": "Endpoint integration policy", + "render": [Function], + }, + Object { + "isVisible": [Function], + "label": "Policy Status", + "render": [Function], + }, + Object { + "field": "agent.version", + "getValues": [Function], + "isVisible": [Function], + "label": "Endpoint version", + }, + Object { + "isVisible": [Function], + "label": "Agent status", + "render": [Function], + }, + ] + `); + + expect( + result.current.map(({ getValues }) => getValues && getValues(mockObservedHostData)) + ).toEqual([ + ['host-id'], + undefined, // First seen doesn't implement getValues + undefined, // Last seen doesn't implement getValues + ['host-ip'], + ['host-mac'], + ['host-platform'], + ['os-name'], + ['host-family'], + ['host-version'], + ['host-architecture'], + undefined, // Max anomaly score by job doesn't implement getValues + ['cloud-provider'], + ['cloud-region'], + ['cloud-instance-id'], + ['cloud-machine-type'], + undefined, // Endpoint integration policy doesn't implement getValues + undefined, // Policy Status doesn't implement getValues + ['endpoint-agent-version'], + undefined, // Agent status doesn't implement getValues ]); }); }); 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 index 8996ee2a17add..67cd23256068f 100644 --- 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 @@ -8,9 +8,9 @@ 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 { getAnomaliesFields } from '../../shared/common'; +import type { EntityTableRows } from '../../shared/components/entity_table/types'; +import type { ObservedEntityData } from '../../shared/components/observed_entity/types'; import { policyFields } from '../fields/endpoint_policy_fields'; import { basicHostFields } from '../fields/basic_host_fields'; import { cloudFields } from '../fields/cloud_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 index dfc29431485de..467a1be82a44c 100644 --- 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 @@ -8,100 +8,74 @@ import { render } from '@testing-library/react'; import React from 'react'; import { TestProviders } from '../../../common/mock'; -import { mockRiskScoreState } from '../mocks'; +import { mockHostRiskScoreState, mockObservedHostData } 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'; +import type { HostPanelProps } from '.'; +import { HostPanel } from '.'; -const mockProps: UserPanelProps = { - userName: 'test', - contextID: 'test-user-panel', +const mockProps: HostPanelProps = { + hostName: 'test', + contextID: 'test-host -panel', scopeId: 'test-scope-id', isDraggable: false, }; jest.mock('../../../common/components/visualization_actions/visualization_embeddable'); -const mockedUseRiskScore = jest.fn().mockReturnValue(mockRiskScoreState); +const mockedHostRiskScore = jest.fn().mockReturnValue(mockHostRiskScoreState); jest.mock('../../../entity_analytics/api/hooks/use_risk_score', () => ({ - useRiskScore: () => mockedUseRiskScore(), + useRiskScore: () => mockedHostRiskScore(), })); -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(), - }) -); +const mockedUseObservedHost = jest.fn().mockReturnValue(mockObservedHostData); jest.mock('./hooks/use_observed_host', () => ({ - useObservedUser: () => mockedUseObservedUser(), + useObservedHost: () => mockedUseObservedHost(), })); -describe('UserPanel', () => { +describe('HostPanel', () => { beforeEach(() => { - mockedUseRiskScore.mockReturnValue(mockRiskScoreState); - mockedUseManagedUser.mockReturnValue(mockManagedUserData); - mockedUseObservedUser.mockReturnValue(mockObservedUser); + mockedHostRiskScore.mockReturnValue(mockHostRiskScoreState); + mockedUseObservedHost.mockReturnValue(mockObservedHostData); }); it('renders', () => { const { getByTestId, queryByTestId } = render( - + ); - expect(getByTestId('user-panel-header')).toBeInTheDocument(); + expect(getByTestId('host-panel-header')).toBeInTheDocument(); expect(queryByTestId('securitySolutionFlyoutLoading')).not.toBeInTheDocument(); expect(getByTestId('securitySolutionFlyoutNavigationExpandDetailButton')).toBeInTheDocument(); }); it('renders loading state when risk score is loading', () => { - mockedUseRiskScore.mockReturnValue({ - ...mockRiskScoreState, + mockedHostRiskScore.mockReturnValue({ + ...mockHostRiskScoreState, 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, + it('renders loading state when observed host is loading', () => { + mockedUseObservedHost.mockReturnValue({ + ...mockObservedHostData, isLoading: true, }); const { getByTestId } = render( - + ); 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 index 054096d194148..9f63328219a4d 100644 --- 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 @@ -20,7 +20,7 @@ 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 type { ObservedEntityData } from '../shared/components/observed_entity/types'; import { useObservedHost } from './hooks/use_observed_host'; import { HostDetailsPanelKey } from '../host_details_left'; @@ -83,22 +83,22 @@ export const HostPanel = ({ contextID, scopeId, hostName, isDraggable }: HostPan }); }, [openLeftPanel, hostRiskData?.host.risk.inputs]); - const useObserved = useObservedHost(hostName); + const observedHost = useObservedHost(hostName); - if (riskScoreState.loading || useObserved.isLoading) { + if (riskScoreState.loading || observedHost.isLoading) { return ; } return ( {({ isLoadingAnomaliesData, anomaliesData, jobNameById }) => { const observedHostWithAnomalies: ObservedEntityData = { - ...useObserved, + ...observedHost, anomalies: { isLoading: isLoadingAnomaliesData, anomalies: anomaliesData, 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 index b2263413c6b36..557f5ead8c5dd 100644 --- 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 @@ -5,10 +5,18 @@ * 2.0. */ +import type { HostMetadataInterface } from '../../../../common/endpoint/types'; +import { EndpointStatus, HostStatus } from '../../../../common/endpoint/types'; 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 type { + HostItem, + HostRiskScore, + RiskScoreEntity, + UserRiskScore, +} from '../../../../common/search_strategy'; +import { HostPolicyResponseActionStatus, RiskSeverity } from '../../../../common/search_strategy'; import { RiskCategories } from '../../../../common/entity_analytics/risk_engine'; +import type { ObservedEntityData } from '../shared/components/observed_entity/types'; const userRiskScore: UserRiskScore = { '@timestamp': '626569200000', @@ -35,7 +43,32 @@ const userRiskScore: UserRiskScore = { oldestAlertTimestamp: '626569200000', }; -export const mockRiskScoreState: RiskScoreState = { +const hostRiskScore: HostRiskScore = { + '@timestamp': '626569200000', + host: { + 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 mockUserRiskScoreState: RiskScoreState = { data: [userRiskScore], inspect: { dsl: [], @@ -49,3 +82,102 @@ export const mockRiskScoreState: RiskScoreState = { isDeprecated: false, loading: false, }; + +export const mockHostRiskScoreState: RiskScoreState = { + data: [hostRiskScore], + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + refetch: () => {}, + totalCount: 0, + isModuleEnabled: true, + isAuthorized: true, + isDeprecated: false, + loading: false, +}; + +export const mockRiskScoreState = { + data: [hostRiskScore], + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + refetch: () => {}, + totalCount: 0, + isModuleEnabled: true, + isAuthorized: true, + isDeprecated: false, + loading: false, +}; + +const hostMetadata: HostMetadataInterface = { + '@timestamp': 1036358673463478, + + agent: { + id: 'endpoint-agent-id', + version: 'endpoint-agent-version', + type: 'endpoint-agent-type', + }, + Endpoint: { + status: EndpointStatus.enrolled, + policy: { + applied: { + name: 'With Eventing', + id: 'C2A9093E-E289-4C0A-AA44-8C32A414FA7A', + endpoint_policy_version: 3, + version: 5, + status: HostPolicyResponseActionStatus.failure, + }, + }, + }, +} as HostMetadataInterface; + +export const mockObservedHost: HostItem = { + host: { + id: ['host-id'], + mac: ['host-mac'], + architecture: ['host-architecture'], + os: { + platform: ['host-platform'], + name: ['os-name'], + version: ['host-version'], + family: ['host-family'], + }, + ip: ['host-ip'], + name: ['host-name'], + }, + cloud: { + instance: { + id: ['cloud-instance-id'], + }, + provider: ['cloud-provider'], + region: ['cloud-region'], + machine: { + type: ['cloud-machine-type'], + }, + }, + endpoint: { + hostInfo: { + metadata: hostMetadata, + host_status: HostStatus.HEALTHY, + last_checkin: 'host-last-checkin', + }, + }, +}; + +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/flyout/entity_details/shared/common.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/common.test.tsx new file mode 100644 index 0000000000000..70b37fd8ba722 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/common.test.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 { getAnomaliesFields } from './common'; +import { emptyMlCapabilities } from '../../../../common/machine_learning/empty_ml_capabilities'; + +const emptyMlCapabilitiesProvider = { + ...emptyMlCapabilities, + capabilitiesFetched: false, +}; + +describe('getAnomaliesFields', () => { + it('returns max anomaly score', () => { + const field = getAnomaliesFields(emptyMlCapabilitiesProvider); + + expect(field[0].label).toBe('Max anomaly score by job'); + }); + + it('hides anomalies field when user has no permissions', () => { + const field = getAnomaliesFields(emptyMlCapabilitiesProvider); + + expect(field[0].isVisible()).toBeFalsy(); + }); + + it('shows anomalies field when user has permissions', () => { + const mlCapabilitiesProvider = { + ...emptyMlCapabilities, + capabilitiesFetched: false, + capabilities: { + ...emptyMlCapabilities.capabilities, + canGetJobs: true, + canGetDatafeeds: true, + canGetCalendars: true, + }, + }; + + const field = getAnomaliesFields(mlCapabilitiesProvider); + + expect(field[0].isVisible()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/common.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/common.tsx similarity index 86% rename from x-pack/plugins/security_solution/public/timelines/components/side_panel/common.tsx rename to x-pack/plugins/security_solution/public/flyout/entity_details/shared/common.tsx index 5601ef2dec99d..95d4758c2c449 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/common.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/common.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import type { ObservedEntityData } from '../../../flyout/entity_details/shared/observed_entity/types'; +import type { ObservedEntityData } from './components/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'; +import { AnomaliesField } from './components/anomalies_field'; export const getAnomaliesFields = (mlCapabilities: MlCapabilitiesProvider) => [ { diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/anomalies_field.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/anomalies_field.test.tsx new file mode 100644 index 0000000000000..514078e5d062a --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/anomalies_field.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { mockAnomalies } from '../../../../common/components/ml/mock'; +import { TestProviders } from '@kbn/timelines-plugin/public/mock'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { AnomaliesField } from './anomalies_field'; + +jest.mock('../../../../common/components/cell_actions', () => { + const actual = jest.requireActual('../../../../common/components/cell_actions'); + return { + ...actual, + SecurityCellActions: () => <>, + }; +}); + +const from = '2022-07-28T08:20:18.966Z'; +const to = '2022-07-28T08:20:18.966Z'; +jest.mock('../../../../common/containers/use_global_time', () => { + const actual = jest.requireActual('../../../../common/containers/use_global_time'); + return { + ...actual, + useGlobalTime: jest.fn().mockReturnValue({ from, to }), + }; +}); + +describe('getAnomaliesFields', () => { + it('returns max anomaly score', () => { + const { getByTestId } = render( + , + { + wrapper: TestProviders, + } + ); + + expect(getByTestId('anomaly-scores')).toBeInTheDocument(); + }); +}); 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/flyout/entity_details/shared/components/anomalies_field.tsx similarity index 90% rename from x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/anomalies_field.tsx rename to x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/anomalies_field.tsx index 8a53c62e22bad..ea5e7b17202f0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/anomalies_field.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/anomalies_field.tsx @@ -7,14 +7,14 @@ import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; +import type { EntityAnomalies } from './observed_entity/types'; 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 }) => { +export const AnomaliesField = ({ anomalies }: { anomalies: EntityAnomalies }) => { const { to, from } = useGlobalTime(); const dispatch = useDispatch(); 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/components/entity_table/columns.tsx similarity index 70% rename from x-pack/plugins/security_solution/public/flyout/entity_details/shared/entity_table/columns.tsx rename to x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/columns.tsx index 8324a7e1308b0..c8f4dddbc5036 100644 --- 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/components/entity_table/columns.tsx @@ -8,10 +8,10 @@ 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 { FormattedMessage } from '@kbn/i18n-react'; +import { DefaultFieldRenderer } from '../../../../../timelines/components/field_renderers/field_renderers'; +import { getEmptyTagValue } from '../../../../../common/components/empty_value'; +import { getSourcererScopeId } from '../../../../../helpers'; import type { BasicEntityData, EntityTableColumns } from './types'; export const getEntityTableColumns = ( @@ -21,7 +21,12 @@ export const getEntityTableColumns = ( data: T ): EntityTableColumns => [ { - name: i18n.FIELD_COLUMN_TITLE, + name: ( + + ), field: 'label', render: (label: string, { field }) => ( ( ), }, { - name: i18n.VALUES_COLUMN_TITLE, + name: ( + + ), field: 'field', render: (field: string | undefined, { getValues, render, renderField }) => { const values = getValues && getValues(data); 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/components/entity_table/index.tsx similarity index 93% rename from x-pack/plugins/security_solution/public/flyout/entity_details/shared/entity_table/index.tsx rename to x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/index.tsx index 1a4c23beffef3..84075071e7a9f 100644 --- 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/components/entity_table/index.tsx @@ -6,7 +6,7 @@ */ import React, { useMemo } from 'react'; -import { BasicTable } from '../../../../common/components/ml/tables/basic_table'; +import { BasicTable } from '../../../../../common/components/ml/tables/basic_table'; import { getEntityTableColumns } from './columns'; import type { BasicEntityData, EntityTableRows } from './types'; 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/components/entity_table/types.ts similarity index 96% rename from x-pack/plugins/security_solution/public/flyout/entity_details/shared/entity_table/types.ts rename to x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/types.ts index 289348fe595de..690a99ec92ce3 100644 --- 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/components/entity_table/types.ts @@ -6,7 +6,7 @@ */ import type { EuiBasicTableColumn } from '@elastic/eui'; -import type { XOR } from '../../../../../common/utility_types'; +import type { XOR } from '../../../../../../common/utility_types'; export type EntityTableRow = XOR< { diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/left_panel/left_panel_content.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_content.tsx similarity index 94% rename from x-pack/plugins/security_solution/public/flyout/entity_details/shared/left_panel/left_panel_content.tsx rename to x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_content.tsx index 3e21377e70457..5a66a5b305611 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/left_panel/left_panel_content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_content.tsx @@ -9,7 +9,7 @@ import { useEuiBackgroundColor } from '@elastic/eui'; import type { VFC } from 'react'; import React, { useMemo } from 'react'; import { css } from '@emotion/react'; -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 { diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/left_panel/left_panel_header.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/flyout/entity_details/shared/left_panel/left_panel_header.tsx rename to x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx index 684efe2c8c2ee..ea62ce25f3ca4 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/left_panel/left_panel_header.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx @@ -9,7 +9,7 @@ import { EuiTab, EuiTabs, useEuiBackgroundColor } from '@elastic/eui'; import type { ReactElement, VFC } from 'react'; import React, { memo } from 'react'; import { css } from '@emotion/react'; -import { FlyoutHeader } from '../../../shared/components/flyout_header'; +import { FlyoutHeader } from '../../../../shared/components/flyout_header'; export type LeftPanelTabsType = Array<{ id: EntityDetailsLeftPanelTab; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/index.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/observed_entity/index.test.tsx similarity index 62% rename from x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/index.test.tsx rename to x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/observed_entity/index.test.tsx index 84ce882d71304..f3cdfefa6c74f 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/index.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/observed_entity/index.test.tsx @@ -8,8 +8,8 @@ import { render } from '@testing-library/react'; import React from 'react'; import { ObservedEntity } from '.'; -import { TestProviders } from '../../../../common/mock'; -import { mockObservedHostData } from '../../../../timelines/components/side_panel/new_host_detail/__mocks__'; +import { TestProviders } from '../../../../../common/mock'; +import { mockObservedHostData } from '../../../mocks'; describe('ObservedHost', () => { const mockProps = { @@ -28,7 +28,7 @@ describe('ObservedHost', () => { ); - expect(getByTestId('observedEntity-data')).toBeInTheDocument(); + expect(getByTestId('observedEntity-accordion')).toBeInTheDocument(); }); it('renders the formatted date', () => { @@ -38,16 +38,6 @@ describe('ObservedHost', () => { ); - expect(getByTestId('observedEntity-data')).toHaveTextContent('Updated Feb 23, 2023'); - }); - - it('renders anomaly score', () => { - const { getByTestId } = render( - - - - ); - - expect(getByTestId('anomaly-score')).toHaveTextContent('17'); + expect(getByTestId('observedEntity-accordion')).toHaveTextContent('Updated Feb 23, 2023'); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/observed_entity/index.tsx similarity index 80% rename from x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/index.tsx rename to x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/observed_entity/index.tsx index 07b6456f3a45a..792ad322e631b 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/observed_entity/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/observed_entity/index.tsx @@ -11,11 +11,10 @@ import React from 'react'; import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EntityTable } from '../entity_table'; -import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; -import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect'; -import * as i18n from './translations'; +import { FormattedRelativePreferenceDate } from '../../../../../common/components/formatted_date'; +import { InspectButton, InspectButtonContainer } from '../../../../../common/components/inspect'; import type { EntityTableRows } from '../entity_table/types'; -import { ONE_WEEK_IN_HOURS } from '../constants'; +import { ONE_WEEK_IN_HOURS } from '../../constants'; import type { ObservedEntityData } from './types'; export const ObservedEntity = ({ @@ -52,7 +51,12 @@ export const ObservedEntity = ({ }} buttonContent={ -

{i18n.OBSERVED_DATA_TITLE}

+

+ +

} extraAction={ @@ -62,7 +66,15 @@ export const ObservedEntity = ({ margin-right: ${euiTheme.size.s}; `} > - + + } + />
{observedData.lastSeen.date && ( useMemo(() => { diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx index 22a112969f78a..f1f7916d3907c 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx @@ -16,10 +16,10 @@ import type { ManagedUserData } from '../../../timelines/components/side_panel/n import type { RiskScoreEntity, UserItem } from '../../../../common/search_strategy'; import { USER_PANEL_RISK_SCORE_QUERY_ID } from '.'; import { FlyoutBody } from '../../shared/components/flyout_body'; -import { ObservedEntity } from '../shared/observed_entity'; -import type { ObservedEntityData } from '../shared/observed_entity/types'; +import { ObservedEntity } from '../shared/components/observed_entity'; +import type { ObservedEntityData } from '../shared/components/observed_entity/types'; import { useObservedUserItems } from './hooks/use_observed_user_items'; -import type { EntityDetailsLeftPanelTab } from '../shared/left_panel/left_panel_header'; +import type { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; interface UserPanelContentProps { observedUser: ObservedEntityData; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/header.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/header.tsx index fea29243d06a8..6390b5a7e49ee 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/header.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/header.tsx @@ -19,7 +19,7 @@ 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'; +import type { ObservedEntityData } from '../shared/components/observed_entity/types'; interface UserPanelHeaderProps { userName: string; 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 index 7a3a6744e0c7b..9c7637d75f543 100644 --- 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 @@ -7,44 +7,47 @@ import { i18n } from '@kbn/i18n'; -export const USER_ID = i18n.translate('xpack.securitySolution.timeline.userDetails.userIdLabel', { +export const USER_ID = i18n.translate('xpack.securitySolution.flyout.entityDetails.user.idLabel', { defaultMessage: 'User ID', }); export const MAX_ANOMALY_SCORE_BY_JOB = i18n.translate( - 'xpack.securitySolution.timeline.userDetails.maxAnomalyScoreByJobLabel', + 'xpack.securitySolution.flyout.entityDetails.user.maxAnomalyScoreByJobLabel', { defaultMessage: 'Max anomaly score by job', } ); export const FIRST_SEEN = i18n.translate( - 'xpack.securitySolution.timeline.userDetails.firstSeenLabel', + 'xpack.securitySolution.flyout.entityDetails.user.firstSeenLabel', { defaultMessage: 'First seen', } ); export const LAST_SEEN = i18n.translate( - 'xpack.securitySolution.timeline.userDetails.lastSeenLabel', + 'xpack.securitySolution.flyout.entityDetails.user.lastSeenLabel', { defaultMessage: 'Last seen', } ); export const OPERATING_SYSTEM_TITLE = i18n.translate( - 'xpack.securitySolution.timeline.userDetails.hostOsNameLabel', + 'xpack.securitySolution.flyout.entityDetails.user.hostOsNameLabel', { defaultMessage: 'Operating system', } ); -export const FAMILY = i18n.translate('xpack.securitySolution.timeline.userDetails.familyLabel', { - defaultMessage: 'Family', -}); +export const FAMILY = i18n.translate( + 'xpack.securitySolution.flyout.entityDetails.user.familyLabel', + { + defaultMessage: 'Family', + } +); export const IP_ADDRESSES = i18n.translate( - 'xpack.securitySolution.timeline.userDetails.ipAddressesLabel', + 'xpack.securitySolution.flyout.entityDetails.user.ipAddressesLabel', { defaultMessage: 'IP addresses', } diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user.ts index 26b04a7f48267..8d7476124efd0 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user.ts +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user.ts @@ -7,7 +7,7 @@ import { useMemo } from 'react'; import { useQueryInspector } from '../../../../common/components/page/manage_query'; -import type { ObservedEntityData } from '../../shared/observed_entity/types'; +import type { ObservedEntityData } from '../../shared/components/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'; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/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 index 7170a60d5213e..1c7b5557dd90a 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/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 @@ -11,7 +11,7 @@ import { mockObservedUser } from '../mocks'; import { useObservedUserItems } from './use_observed_user_items'; describe('useManagedUserItems', () => { - it('returns managed user items for Entra user', () => { + it('returns observed user fields', () => { const { result } = renderHook(() => useObservedUserItems(mockObservedUser), { wrapper: TestProviders, }); @@ -20,43 +20,58 @@ describe('useManagedUserItems', () => { { field: 'user.id', label: 'User ID', - values: ['1234', '321'], + getValues: expect.any(Function), }, { field: 'user.domain', label: 'Domain', - values: ['test domain', 'another test domain'], - }, - { - field: 'anomalies', - label: 'Max anomaly score by job', - values: mockObservedUser.anomalies, + getValues: expect.any(Function), }, { field: '@timestamp', label: 'First seen', - values: ['2023-02-23T20:03:17.489Z'], + getValues: expect.any(Function), }, { field: '@timestamp', label: 'Last seen', - values: ['2023-02-23T20:03:17.489Z'], + getValues: expect.any(Function), }, { field: 'host.os.name', label: 'Operating system', - values: ['testOs'], + getValues: expect.any(Function), }, { field: 'host.os.family', label: 'Family', - values: ['testFamily'], + + getValues: expect.any(Function), }, { field: 'host.ip', label: 'IP addresses', - values: ['10.0.0.1', '127.0.0.1'], + + getValues: expect.any(Function), + }, + { + label: 'Max anomaly score by job', + isVisible: expect.any(Function), + render: expect.any(Function), }, ]); + + expect(result.current.map(({ getValues }) => getValues && getValues(mockObservedUser))).toEqual( + [ + ['1234', '321'], // id + ['test domain', 'another test domain'], // domain + ['2023-02-23T20:03:17.489Z'], // First seen + ['2023-02-23T20:03:17.489Z'], // Last seen + ['testOs'], // OS name + ['testFamily'], // os family + ['10.0.0.1', '127.0.0.1'], // IP addresses + undefined, // Max anomaly score by job doesn't implement getValues + ] + ); }); }); 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 index ace75d20880bb..a70da4214a8ee 100644 --- 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 @@ -8,10 +8,10 @@ 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 { getAnomaliesFields } from '../../shared/common'; import * as i18n from './translations'; -import type { ObservedEntityData } from '../../shared/observed_entity/types'; -import type { EntityTableRows } from '../../shared/entity_table/types'; +import type { ObservedEntityData } from '../../shared/components/observed_entity/types'; +import type { EntityTableRows } from '../../shared/components/entity_table/types'; const basicUserFields: EntityTableRows> = [ { diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.test.tsx index 1c74e4ed23ea5..9961b3ea086e2 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.test.tsx @@ -10,12 +10,12 @@ import React from 'react'; import { TestProviders } from '../../../common/mock'; import type { UserPanelProps } from '.'; import { UserPanel } from '.'; -import { mockRiskScoreState } from './mocks'; import { mockManagedUserData, - mockObservedUser, + mockRiskScoreState, } from '../../../timelines/components/side_panel/new_user_detail/__mocks__'; +import { mockObservedUser } from './mocks'; const mockProps: UserPanelProps = { userName: 'test', @@ -41,12 +41,9 @@ jest.mock( }) ); -jest.mock( - '../../../timelines/components/side_panel/new_user_detail/hooks/use_observed_user', - () => ({ - useObservedUser: () => mockedUseObservedUser(), - }) -); +jest.mock('./hooks/use_observed_user', () => ({ + useObservedUser: () => mockedUseObservedUser(), +})); describe('UserPanel', () => { beforeEach(() => { 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 043be4f4da90e..abe3ee4793016 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 @@ -24,7 +24,7 @@ import { UserPanelContent } from './content'; import { UserPanelHeader } from './header'; import { UserDetailsPanelKey } from '../user_details_left'; import { useObservedUser } from './hooks/use_observed_user'; -import type { EntityDetailsLeftPanelTab } from '../shared/left_panel/left_panel_header'; +import type { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; export interface UserPanelProps extends Record { contextID: string; 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 c734b7849226c..b58c94c5772ff 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 @@ -7,7 +7,7 @@ import { mockAnomalies } from '../../../../common/components/ml/mock'; import type { UserItem } from '../../../../../common/search_strategy'; -import type { ObservedEntityData } from '../../shared/observed_entity/types'; +import type { ObservedEntityData } from '../../shared/components/observed_entity/types'; const anomaly = mockAnomalies.anomalies[0]; 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 deleted file mode 100644 index 1d2c862b00a92..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_host_detail/__mocks__/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { 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 deleted file mode 100644 index 7486b9dcd0973..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_host_detail/translations.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -// TODO 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', - } -); From d3db40ec625d80da931623068d1853360dcfd885 Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Mon, 1 Jan 2024 20:33:33 +0200 Subject: [PATCH 03/13] adapt risk summary to both entity flyouts --- .../risk_summary_flyout/risk_summary.test.tsx | 5 + .../risk_summary_flyout/risk_summary.tsx | 376 +++++++++--------- .../entity_details/host_right/content.tsx | 15 +- .../entity_details/user_right/content.tsx | 4 +- 4 files changed, 216 insertions(+), 184 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx index 5862ac7cd9464..81a9159a4f44a 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine/types'; import { render } from '@testing-library/react'; import React from 'react'; import { TestProviders } from '../../../common/mock'; @@ -21,6 +22,7 @@ describe('RiskSummary', () => { riskScoreData={mockRiskScoreState} queryId={'testQuery'} openDetailsPanel={() => {}} + entity={RiskScoreEntity.user} /> ); @@ -37,6 +39,7 @@ describe('RiskSummary', () => { riskScoreData={{ ...mockRiskScoreState, data: undefined }} queryId={'testQuery'} openDetailsPanel={() => {}} + entity={RiskScoreEntity.user} /> ); @@ -50,6 +53,7 @@ describe('RiskSummary', () => { riskScoreData={mockRiskScoreState} queryId={'testQuery'} openDetailsPanel={() => {}} + entity={RiskScoreEntity.user} /> ); @@ -64,6 +68,7 @@ describe('RiskSummary', () => { riskScoreData={mockRiskScoreState} queryId={'testQuery'} openDetailsPanel={() => {}} + entity={RiskScoreEntity.user} /> ); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx index 50259cd6ca134..21467afd5c718 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx @@ -21,6 +21,10 @@ import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; import { euiThemeVars } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; +import type { + HostRiskScore, + UserRiskScore, +} from '../../../../common/search_strategy/security_solution/risk_score'; import { UserDetailsLeftPanelTab } from '../../../flyout/entity_details/user_details_left/tabs'; import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; import { ONE_WEEK_IN_HOURS } from '../../../timelines/components/side_panel/new_user_detail/constants'; @@ -31,10 +35,11 @@ import { ExpandablePanel } from '../../../flyout/shared/components/expandable_pa import type { RiskScoreState } from '../../api/hooks/use_risk_score'; import { getRiskScoreSummaryAttributes } from '../../lens_attributes/risk_score_summary'; -export interface RiskSummaryProps { - riskScoreData: RiskScoreState; +export interface RiskSummaryProps { + riskScoreData: RiskScoreState; queryId: string; openDetailsPanel: (tab: UserDetailsLeftPanelTab) => void; + entity: T; } interface TableItem { @@ -44,203 +49,214 @@ interface TableItem { const LENS_VISUALIZATION_HEIGHT = 126; // Static height in pixels specified by design const LAST_30_DAYS = { from: 'now-30d', to: 'now' }; -export const RiskSummary = React.memo( - ({ riskScoreData, queryId, openDetailsPanel }: RiskSummaryProps) => { - const { data: userRisk } = riskScoreData; - const userRiskData = userRisk && userRisk.length > 0 ? userRisk[0] : undefined; - const { euiTheme } = useEuiTheme(); +const RiskSummaryComponent = ({ + riskScoreData, + queryId, + openDetailsPanel, + entity, +}: RiskSummaryProps) => { + const { data } = riskScoreData; + const riskData = data && data.length > 0 ? data[0] : undefined; + const { euiTheme } = useEuiTheme(); - const lensAttributes = useMemo(() => { - return getRiskScoreSummaryAttributes({ - severity: userRiskData?.user?.risk?.calculated_level, - query: `user.name: ${userRiskData?.user?.name}`, - spaceId: 'default', - riskEntity: RiskScoreEntity.user, - }); - }, [userRiskData]); + const entityData = (() => { + if (!riskData) return; + if (entity === RiskScoreEntity.user) return (riskData as UserRiskScore).user; + if (entity === RiskScoreEntity.host) return (riskData as HostRiskScore).host; + })(); - const columns: Array> = useMemo( - () => [ - { - field: 'category', - name: ( + const lensAttributes = useMemo(() => { + return getRiskScoreSummaryAttributes({ + severity: entityData?.risk?.calculated_level, + query: `user.name: ${entityData?.name}`, + spaceId: 'default', + riskEntity: RiskScoreEntity.user, + }); + }, [entityData]); + + const columns: Array> = useMemo( + () => [ + { + field: 'category', + name: ( + + ), + truncateText: false, + mobileOptions: { show: true }, + sortable: true, + }, + { + field: 'count', + name: ( + + ), + truncateText: false, + mobileOptions: { show: true }, + sortable: true, + dataType: 'number', + }, + ], + [] + ); + + const xsFontSize = useEuiFontSize('xxs').fontSize; + + const items: TableItem[] = useMemo( + () => [ + { + category: i18n.translate('xpack.securitySolution.flyout.entityDetails.alertsGroupLabel', { + defaultMessage: 'Alerts', + }), + count: entityData?.risk.inputs?.length ?? 0, + }, + ], + [entityData?.risk.inputs?.length] + ); + + return ( + +

- ), - truncateText: false, - mobileOptions: { show: true }, - sortable: true, - }, - { - field: 'count', - name: ( +

+ + } + extraAction={ + + {riskData && ( + ), + }} /> - ), - truncateText: false, - mobileOptions: { show: true }, - sortable: true, - dataType: 'number', - }, - ], - [] - ); - - const xsFontSize = useEuiFontSize('xxs').fontSize; - - const items: TableItem[] = useMemo( - () => [ - { - category: i18n.translate('xpack.securitySolution.flyout.entityDetails.alertsGroupLabel', { - defaultMessage: 'Alerts', - }), - count: userRiskData?.user.risk.inputs?.length ?? 0, - }, - ], - [userRiskData?.user.risk.inputs?.length] - ); + )} + + } + > + - return ( - -

- -

- - } - extraAction={ - - {userRiskData && ( - - ), - }} - /> - )} - - } - > - - - + ), + link: { + callback: () => openDetailsPanel(UserDetailsLeftPanelTab.RISK_INPUTS), + tooltip: ( ), - link: { - callback: () => openDetailsPanel(UserDetailsLeftPanelTab.RISK_INPUTS), - tooltip: ( - + + +
+ {riskData && ( + + } /> - ), - }, - iconType: 'arrowStart', - }} - expand={{ - expandable: false, - }} - > - - + )} +
+
+ +
- {userRiskData && ( - - } - /> - )} -
-
- -
-
- - } - /> -
- + } />
-
-
-
-
- -
- ); - } -); + + + + + + + +
+ ); +}; + +export const RiskSummary = React.memo(RiskSummaryComponent); RiskSummary.displayName = 'RiskSummary'; 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 index 4a5ddcb3f181f..fb768fd9de26f 100644 --- 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 @@ -8,11 +8,13 @@ import { EuiHorizontalRule } from '@elastic/eui'; import React from 'react'; +import { RiskSummary } from '../../../entity_analytics/components/risk_summary_flyout/risk_summary'; import type { RiskScoreState } from '../../../entity_analytics/api/hooks/use_risk_score'; -import type { HostItem, RiskScoreEntity } from '../../../../common/search_strategy'; +import { RiskScoreEntity } from '../../../../common/search_strategy'; +import type { HostItem } from '../../../../common/search_strategy'; import { FlyoutBody } from '../../shared/components/flyout_body'; import { ObservedEntity } from '../shared/components/observed_entity'; -import { HOST_PANEL_OBSERVED_HOST_QUERY_ID } from '.'; +import { HOST_PANEL_OBSERVED_HOST_QUERY_ID, HOST_PANEL_RISK_SCORE_QUERY_ID } from '.'; import type { ObservedEntityData } from '../shared/components/observed_entity/types'; import { useObservedHostFields } from './hooks/use_observed_host_fields'; @@ -37,7 +39,14 @@ export const HostPanelContent = ({ {riskScoreState.isModuleEnabled && riskScoreState.data?.length !== 0 && ( <> - {/* TODO */} + { + {}} + /> + } )} diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx index f1f7916d3907c..7cfff92b02421 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx @@ -13,7 +13,8 @@ import { RiskSummary } from '../../../entity_analytics/components/risk_summary_f import type { RiskScoreState } from '../../../entity_analytics/api/hooks/use_risk_score'; import { ManagedUser } from '../../../timelines/components/side_panel/new_user_detail/managed_user'; import type { ManagedUserData } from '../../../timelines/components/side_panel/new_user_detail/types'; -import type { RiskScoreEntity, UserItem } from '../../../../common/search_strategy'; +import { RiskScoreEntity } from '../../../../common/search_strategy'; +import type { UserItem } from '../../../../common/search_strategy'; import { USER_PANEL_RISK_SCORE_QUERY_ID } from '.'; import { FlyoutBody } from '../../shared/components/flyout_body'; import { ObservedEntity } from '../shared/components/observed_entity'; @@ -50,6 +51,7 @@ export const UserPanelContent = ({ riskScoreData={riskScoreState} queryId={USER_PANEL_RISK_SCORE_QUERY_ID} openDetailsPanel={openDetailsPanel} + entity={RiskScoreEntity.user} /> From 9b10b2e222d7dbd0c96528c18cec5ae9e31f2d38 Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Tue, 2 Jan 2024 12:10:09 +0200 Subject: [PATCH 04/13] opening panel --- .../risk_summary_flyout/risk_summary.tsx | 6 ++-- .../entity_details/host_right/content.tsx | 5 ++- .../entity_details/host_right/index.tsx | 31 +++++++++++++------ 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx index 21467afd5c718..fdb5b8559f7b8 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx @@ -21,11 +21,11 @@ import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; import { euiThemeVars } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; +import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; import type { HostRiskScore, UserRiskScore, } from '../../../../common/search_strategy/security_solution/risk_score'; -import { UserDetailsLeftPanelTab } from '../../../flyout/entity_details/user_details_left/tabs'; import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; import { ONE_WEEK_IN_HOURS } from '../../../timelines/components/side_panel/new_user_detail/constants'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; @@ -38,7 +38,7 @@ import { getRiskScoreSummaryAttributes } from '../../lens_attributes/risk_score_ export interface RiskSummaryProps { riskScoreData: RiskScoreState; queryId: string; - openDetailsPanel: (tab: UserDetailsLeftPanelTab) => void; + openDetailsPanel: (tab: EntityDetailsLeftPanelTab) => void; entity: T; } @@ -174,7 +174,7 @@ const RiskSummaryComponent = ({ /> ), link: { - callback: () => openDetailsPanel(UserDetailsLeftPanelTab.RISK_INPUTS), + callback: () => openDetailsPanel(EntityDetailsLeftPanelTab.RISK_INPUTS), tooltip: ( ; @@ -24,6 +25,7 @@ interface HostPanelContentProps { contextID: string; scopeId: string; isDraggable: boolean; + openDetailsPanel: (tab: EntityDetailsLeftPanelTab) => void; } export const HostPanelContent = ({ @@ -32,6 +34,7 @@ export const HostPanelContent = ({ contextID, scopeId, isDraggable, + openDetailsPanel, }: HostPanelContentProps) => { const observedFields = useObservedHostFields(observedHost); @@ -44,7 +47,7 @@ export const HostPanelContent = ({ riskScoreData={riskScoreState} queryId={HOST_PANEL_RISK_SCORE_QUERY_ID} entity={RiskScoreEntity.host} - openDetailsPanel={() => {}} + openDetailsPanel={openDetailsPanel} /> } 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 index 9f63328219a4d..783a9ce598381 100644 --- 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 @@ -8,6 +8,7 @@ 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'; @@ -23,6 +24,7 @@ import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anom import type { ObservedEntityData } from '../shared/components/observed_entity/types'; import { useObservedHost } from './hooks/use_observed_host'; import { HostDetailsPanelKey } from '../host_details_left'; +import type { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; export interface HostPanelProps extends Record { contextID: string; @@ -72,17 +74,25 @@ export const HostPanel = ({ contextID, scopeId, hostName, isDraggable }: HostPan setQuery, }); - const openPanel = useCallback(() => { - openLeftPanel({ - id: HostDetailsPanelKey, - params: { - riskInputs: { - alertIds: hostRiskData?.host.risk.inputs?.map(({ id }) => id) ?? [], + const openTabPanel = useCallback( + (tab?: EntityDetailsLeftPanelTab) => { + openLeftPanel({ + id: HostDetailsPanelKey, + params: { + riskInputs: { + alertIds: hostRiskData?.host.risk.inputs?.map(({ id }) => id) ?? [], + host: { + name: hostName, + }, + }, + path: tab ? { tab } : undefined, }, - }, - }); - }, [openLeftPanel, hostRiskData?.host.risk.inputs]); + }); + }, + [openLeftPanel, hostRiskData?.host.risk.inputs, hostName] + ); + const openDefaultPanel = useCallback(() => openTabPanel(), [openTabPanel]); const observedHost = useObservedHost(hostName); if (riskScoreState.loading || observedHost.isLoading) { @@ -110,7 +120,7 @@ export const HostPanel = ({ contextID, scopeId, hostName, isDraggable }: HostPan <> ); From a46a57ef921850c034655bed935e910168ea0e00 Mon Sep 17 00:00:00 2001 From: machadoum Date: Tue, 2 Jan 2024 11:24:45 +0100 Subject: [PATCH 05/13] Add more tests and fix storybook --- .../common/mock/storybook_providers.tsx | 17 ++-- .../host_details_left/index.test.tsx | 38 ++++++++ .../host_right/content.stories.tsx | 86 +++-------------- .../fields/endpoint_policy_fields.test.tsx | 61 ++++++++++++ .../host_right/fields/translations.ts | 14 --- .../entity_details/host_right/header.test.tsx | 91 ++++-------------- .../flyout/entity_details/mocks/index.ts | 2 +- .../components/anomalies_field.test.tsx | 12 +-- .../components/entity_table/columns.tsx | 2 + .../components/entity_table/index.test.tsx | 94 +++++++++++++++++++ .../user_right/content.stories.tsx | 2 +- .../body/renderers/host_name.test.tsx | 40 ++++++++ .../timeline/body/renderers/host_name.tsx | 1 + 13 files changed, 280 insertions(+), 180 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/host_right/fields/endpoint_policy_fields.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/mock/storybook_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/storybook_providers.tsx index 6fb3ca1ff0f5b..40b9ce71419db 100644 --- a/x-pack/plugins/security_solution/public/common/mock/storybook_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/storybook_providers.tsx @@ -21,6 +21,7 @@ import { mockGlobalState } from './global_state'; import { SUB_PLUGINS_REDUCER } from './utils'; import { createSecuritySolutionStorageMock } from './mock_local_storage'; import type { StartServices } from '../../types'; +import { ReactQueryClientProvider } from '../containers/query_client/query_client_provider'; export const kibanaObservable = new BehaviorSubject({} as unknown as StartServices); @@ -106,13 +107,15 @@ export const StorybookProviders: React.FC = ({ children }) => { - Promise.resolve([])}> - - ({ eui: euiLightVars, darkMode: false })}> - {children} - - - + + Promise.resolve([])}> + + ({ eui: euiLightVars, darkMode: false })}> + {children} + + + + diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx new file mode 100644 index 0000000000000..82cf0f8ff9f56 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx @@ -0,0 +1,38 @@ +/* + * 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 { RISK_INPUTS_TAB_TEST_ID } from '../../../entity_analytics/components/entity_details_flyout'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { HostDetailsPanel } from '.'; +import { TestProviders } from '../../../common/mock'; + +describe('HostDetailsPanel', () => { + it('render risk inputs panel', () => { + const { getByTestId } = render( + , + { wrapper: TestProviders } + ); + expect(getByTestId(RISK_INPUTS_TAB_TEST_ID)).toBeInTheDocument(); + }); + + it("doesn't render risk inputs panel when no alerts ids are provided", () => { + const { queryByTestId } = render( + , + { wrapper: TestProviders } + ); + expect(queryByTestId(RISK_INPUTS_TAB_TEST_ID)).not.toBeInTheDocument(); + }); +}); 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 index b8975b747ea44..17df523d4e21b 100644 --- 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 @@ -11,12 +11,9 @@ 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'; +import { mockRiskScoreState } from '../../../timelines/components/side_panel/new_user_detail/__mocks__'; +import { HostPanelContent } from './content'; +import { mockObservedHostData } from '../mocks'; const flyoutContextValue = { openLeftPanel: () => window.alert('openLeftPanel called'), @@ -25,7 +22,7 @@ const flyoutContextValue = { const riskScoreData = { ...mockRiskScoreState, data: [] }; -storiesOf('Components/UserPanelContent', module) +storiesOf('Components/HostPanelContent', module) .addDecorator((storyFn) => ( @@ -36,37 +33,8 @@ storiesOf('Components/UserPanelContent', module) )) .add('default', () => ( - - )) - .add('integration disabled', () => ( - - )) - .add('no managed data', () => ( - )) .add('no observed data', () => ( - )) .add('loading', () => ( - <>{el}; + +jest.mock( + '../../../../management/hooks/response_actions/use_get_endpoint_pending_actions_summary', + () => { + const original = jest.requireActual( + '../../../../management/hooks/response_actions/use_get_endpoint_pending_actions_summary' + ); + return { + ...original, + useGetEndpointPendingActionsSummary: () => ({ + pendingActions: [], + isLoading: false, + isError: false, + isTimeout: false, + fetch: jest.fn(), + }), + }; + } +); + +describe('Endpoint Policy Fields', () => { + it('renders policy name', () => { + const policyName = policyFields[0]; + + const { container } = render(); + + expect(container).toHaveTextContent('policy-name'); + }); + + it('renders policy status', () => { + const policyStatus = policyFields[1]; + + const { container } = render(); + + expect(container).toHaveTextContent('failure'); + }); + + it('renders agent status', () => { + const agentStatus = policyFields[3]; + + const { container } = render(, { + wrapper: TestProviders, + }); + + expect(container).toHaveTextContent('Healthy'); + }); +}); 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 index f4d64e7246a90..dac45a3a6202c 100644 --- 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 @@ -28,20 +28,6 @@ export const LAST_SEEN = i18n.translate( } ); -export const HOST_RISK_SCORE = i18n.translate( - 'xpack.securitySolution.flyout.entityDetails.host.hostRiskScoreTitle', - { - defaultMessage: 'Host risk score', - } -); - -export const HOST_RISK_LEVEL = i18n.translate( - 'xpack.securitySolution.flyout.entityDetails.host.hostRiskLevel', - { - defaultMessage: 'Host risk level', - } -); - export const IP_ADDRESSES = i18n.translate( 'xpack.securitySolution.flyout.entityDetails.host.ipAddressesTitle', { 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 index 2f1ada7447dd6..418ec64cb6709 100644 --- 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 @@ -5,45 +5,39 @@ * 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'; +import { HostPanelHeader } from './header'; +import { mockObservedHostData } from '../mocks'; const mockProps = { - userName: 'test', - managedUser: mockManagedUserData, - observedUser: mockObservedUser, + hostName: 'test', + observedHost: mockObservedHostData, }; jest.mock('../../../common/components/visualization_actions/visualization_embeddable'); -describe('UserDetailsContent', () => { +describe('HostPanelHeader', () => { it('renders', () => { const { getByTestId } = render( - + ); - expect(getByTestId('user-panel-header')).toBeInTheDocument(); + expect(getByTestId('host-panel-header')).toBeInTheDocument(); }); - it('renders observed user date when it is bigger than managed user date', () => { + it('renders observed date', () => { const futureDay = '2989-03-07T20:00:00.000Z'; const { getByTestId } = render( - { ); - expect(getByTestId('user-panel-header-lastSeen').textContent).toContain('Mar 7, 2989'); + expect(getByTestId('host-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', () => { + it('renders observed badge when lastSeen is defined', () => { const { getByTestId } = render( - + ); - expect(getByTestId('user-panel-header-observed-badge')).toBeInTheDocument(); - expect(getByTestId('user-panel-header-managed-badge')).toBeInTheDocument(); + expect(getByTestId('host-panel-header-observed-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(); + expect(queryByTestId('host-panel-header-observed-badge')).not.toBeInTheDocument(); }); }); 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 index 557f5ead8c5dd..c66f16f5320e4 100644 --- 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 @@ -125,7 +125,7 @@ const hostMetadata: HostMetadataInterface = { status: EndpointStatus.enrolled, policy: { applied: { - name: 'With Eventing', + name: 'policy-name', id: 'C2A9093E-E289-4C0A-AA44-8C32A414FA7A', endpoint_policy_version: 3, version: 5, diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/anomalies_field.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/anomalies_field.test.tsx index 514078e5d062a..c8a2cdbb71dae 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/anomalies_field.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/anomalies_field.test.tsx @@ -5,10 +5,10 @@ * 2.0. */ import { mockAnomalies } from '../../../../common/components/ml/mock'; -import { TestProviders } from '@kbn/timelines-plugin/public/mock'; import { render } from '@testing-library/react'; import React from 'react'; import { AnomaliesField } from './anomalies_field'; +import { TestProviders } from '../../../../common/mock'; jest.mock('../../../../common/components/cell_actions', () => { const actual = jest.requireActual('../../../../common/components/cell_actions'); @@ -18,16 +18,6 @@ jest.mock('../../../../common/components/cell_actions', () => { }; }); -const from = '2022-07-28T08:20:18.966Z'; -const to = '2022-07-28T08:20:18.966Z'; -jest.mock('../../../../common/containers/use_global_time', () => { - const actual = jest.requireActual('../../../../common/containers/use_global_time'); - return { - ...actual, - useGlobalTime: jest.fn().mockReturnValue({ from, to }), - }; -}); - describe('getAnomaliesFields', () => { it('returns max anomaly score', () => { const { getByTestId } = render( diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/columns.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/columns.tsx index c8f4dddbc5036..bd0891c962d1e 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/columns.tsx @@ -30,6 +30,7 @@ export const getEntityTableColumns = ( field: 'label', render: (label: string, { field }) => ( ( isDraggable={isDraggable} sourcererScopeId={getSourcererScopeId(scopeId)} render={renderField} + data-test-subj="entity-table-value" /> ); } diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/index.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/index.test.tsx new file mode 100644 index 0000000000000..d6243abb39e9f --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/index.test.tsx @@ -0,0 +1,94 @@ +/* + * 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 { EntityTable } from '.'; +import { TestProviders } from '../../../../../common/mock'; +import type { BasicEntityData, EntityTableRow } from './types'; + +const renderedFieldValue = 'testValue1'; + +const testField: EntityTableRow = { + label: 'testLabel', + field: 'testField', + getValues: (data: unknown) => [renderedFieldValue], + renderField: (field: string) => <>{field}, +}; + +const mockProps = { + contextID: 'testContextID', + scopeId: 'testScopeId', + isDraggable: false, + data: { isLoading: false }, + entityFields: [testField], +}; + +describe('EntityTable', () => { + it('renders correctly', () => { + const { queryByTestId, queryAllByTestId } = render(, { + wrapper: TestProviders, + }); + + expect(queryByTestId('entity-table')).toBeInTheDocument(); + expect(queryAllByTestId('entity-table-label')).toHaveLength(1); + }); + + it("it doesn't render fields when isVisible returns false", () => { + const props = { + ...mockProps, + entityFields: [ + { + ...testField, + isVisible: () => false, + }, + ], + }; + + const { queryAllByTestId } = render(, { + wrapper: TestProviders, + }); + + expect(queryAllByTestId('entity-table-label')).toHaveLength(0); + }); + + it('it renders the field label', () => { + const { queryByTestId } = render(, { + wrapper: TestProviders, + }); + + expect(queryByTestId('entity-table-label')).toHaveTextContent('testLabel'); + }); + + it('it renders the field value', () => { + const { queryByTestId } = render(, { + wrapper: TestProviders, + }); + + expect(queryByTestId('DefaultFieldRendererComponent')).toHaveTextContent(renderedFieldValue); + }); + + it('it call render function when field is undefined', () => { + const props = { + ...mockProps, + entityFields: [ + { + label: 'testLabel', + render: (data: unknown) => ( + {'test-custom-render'} + ), + }, + ], + }; + + const { queryByTestId } = render(, { + wrapper: TestProviders, + }); + + expect(queryByTestId('test-custom-render')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.stories.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.stories.tsx index f4a88ce1c7cd3..f0589497e452e 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.stories.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.stories.tsx @@ -13,10 +13,10 @@ 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'; +import { mockObservedUser } from './mocks'; const flyoutContextValue = { openLeftPanel: () => window.alert('openLeftPanel called'), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx index 1535b05a97a4f..437f8be9de10c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx @@ -17,6 +17,23 @@ import { StatefulEventContext } from '../../../../../common/components/events_vi import { createTelemetryServiceMock } from '../../../../../common/lib/telemetry/telemetry_service.mock'; const mockedTelemetry = createTelemetryServiceMock(); +const mockUseIsExperimentalFeatureEnabled = jest.fn(); +const mockOpenRightPanel = jest.fn(); + +jest.mock('../../../../../common/hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: () => mockUseIsExperimentalFeatureEnabled, +})); + +jest.mock('@kbn/expandable-flyout/src/context', () => { + const original = jest.requireActual('@kbn/expandable-flyout/src/context'); + + return { + ...original, + useExpandableFlyoutContext: () => ({ + openRightPanel: mockOpenRightPanel, + }), + }; +}); jest.mock('react-redux', () => { const origin = jest.requireActual('react-redux'); @@ -197,4 +214,27 @@ describe('HostName', () => { expect(toggleExpandedDetail).not.toHaveBeenCalled(); }); }); + + test('it should open expandable flyout if timeline is not in context and experimental flag is enabled', async () => { + mockUseIsExperimentalFeatureEnabled.mockReturnValue(true); + const context = { + enableHostDetailsFlyout: true, + enableIpDetailsFlyout: true, + timelineID: 'fake-timeline', + tabType: TimelineTabs.query, + }; + const wrapper = mount( + + + + + + ); + + wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click'); + await waitFor(() => { + expect(mockOpenRightPanel).toHaveBeenCalled(); + expect(toggleExpandedDetail).not.toHaveBeenCalled(); + }); + }); }); 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 ac46f97ad2793..2f343373a4616 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 @@ -65,6 +65,7 @@ const HostNameComponent: React.FC = ({ if (onClick) { onClick(); } + if (eventContext && isInTimelineContext) { const { timelineID, tabType } = eventContext; From 80f6141fe0e305556e4f4eefbf910854a1d5ab79 Mon Sep 17 00:00:00 2001 From: machadoum Date: Tue, 2 Jan 2024 12:06:17 +0100 Subject: [PATCH 06/13] Improve entity type check --- .../risk_summary_flyout/risk_summary.test.tsx | 7 +------ .../risk_summary_flyout/risk_summary.tsx | 19 +++++++++++-------- .../entity_details/host_right/content.tsx | 4 +--- .../entity_details/user_right/content.tsx | 4 +--- 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx index 81a9159a4f44a..2ac1f5a7512a8 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx @@ -5,11 +5,10 @@ * 2.0. */ -import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine/types'; +import { mockRiskScoreState } from '../../../flyout/entity_details/mocks'; import { render } from '@testing-library/react'; import React from 'react'; import { TestProviders } from '../../../common/mock'; -import { mockRiskScoreState } from '../../../flyout/entity_details/user_right/mocks'; import { RiskSummary } from './risk_summary'; jest.mock('../../../common/components/visualization_actions/visualization_embeddable'); @@ -22,7 +21,6 @@ describe('RiskSummary', () => { riskScoreData={mockRiskScoreState} queryId={'testQuery'} openDetailsPanel={() => {}} - entity={RiskScoreEntity.user} /> ); @@ -39,7 +37,6 @@ describe('RiskSummary', () => { riskScoreData={{ ...mockRiskScoreState, data: undefined }} queryId={'testQuery'} openDetailsPanel={() => {}} - entity={RiskScoreEntity.user} /> ); @@ -53,7 +50,6 @@ describe('RiskSummary', () => { riskScoreData={mockRiskScoreState} queryId={'testQuery'} openDetailsPanel={() => {}} - entity={RiskScoreEntity.user} /> ); @@ -68,7 +64,6 @@ describe('RiskSummary', () => { riskScoreData={mockRiskScoreState} queryId={'testQuery'} openDetailsPanel={() => {}} - entity={RiskScoreEntity.user} /> ); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx index fdb5b8559f7b8..cf5ce4c6281a2 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx @@ -39,7 +39,6 @@ export interface RiskSummaryProps { riskScoreData: RiskScoreState; queryId: string; openDetailsPanel: (tab: EntityDetailsLeftPanelTab) => void; - entity: T; } interface TableItem { @@ -49,22 +48,26 @@ interface TableItem { const LENS_VISUALIZATION_HEIGHT = 126; // Static height in pixels specified by design const LAST_30_DAYS = { from: 'now-30d', to: 'now' }; +function isUserRiskData(riskData: UserRiskScore | HostRiskScore): riskData is UserRiskScore { + return (riskData as UserRiskScore).user !== undefined; +} + +const getEntityData = (riskData: UserRiskScore | HostRiskScore | undefined) => { + if (!riskData) return; + if (isUserRiskData(riskData)) return riskData.user; + return riskData.host; +}; + const RiskSummaryComponent = ({ riskScoreData, queryId, openDetailsPanel, - entity, }: RiskSummaryProps) => { const { data } = riskScoreData; const riskData = data && data.length > 0 ? data[0] : undefined; + const entityData = getEntityData(riskData); const { euiTheme } = useEuiTheme(); - const entityData = (() => { - if (!riskData) return; - if (entity === RiskScoreEntity.user) return (riskData as UserRiskScore).user; - if (entity === RiskScoreEntity.host) return (riskData as HostRiskScore).host; - })(); - const lensAttributes = useMemo(() => { return getRiskScoreSummaryAttributes({ severity: entityData?.risk?.calculated_level, 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 index ac8416ac2518c..eb7d5f3fda26d 100644 --- 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 @@ -10,8 +10,7 @@ import { EuiHorizontalRule } from '@elastic/eui'; import React from 'react'; import { RiskSummary } from '../../../entity_analytics/components/risk_summary_flyout/risk_summary'; import type { RiskScoreState } from '../../../entity_analytics/api/hooks/use_risk_score'; -import { RiskScoreEntity } from '../../../../common/search_strategy'; -import type { HostItem } from '../../../../common/search_strategy'; +import type { RiskScoreEntity, HostItem } from '../../../../common/search_strategy'; import { FlyoutBody } from '../../shared/components/flyout_body'; import { ObservedEntity } from '../shared/components/observed_entity'; import { HOST_PANEL_OBSERVED_HOST_QUERY_ID, HOST_PANEL_RISK_SCORE_QUERY_ID } from '.'; @@ -46,7 +45,6 @@ export const HostPanelContent = ({ } diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx index 7cfff92b02421..f1f7916d3907c 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx @@ -13,8 +13,7 @@ import { RiskSummary } from '../../../entity_analytics/components/risk_summary_f import type { RiskScoreState } from '../../../entity_analytics/api/hooks/use_risk_score'; import { ManagedUser } from '../../../timelines/components/side_panel/new_user_detail/managed_user'; import type { ManagedUserData } from '../../../timelines/components/side_panel/new_user_detail/types'; -import { RiskScoreEntity } from '../../../../common/search_strategy'; -import type { UserItem } from '../../../../common/search_strategy'; +import type { RiskScoreEntity, UserItem } from '../../../../common/search_strategy'; import { USER_PANEL_RISK_SCORE_QUERY_ID } from '.'; import { FlyoutBody } from '../../shared/components/flyout_body'; import { ObservedEntity } from '../shared/components/observed_entity'; @@ -51,7 +50,6 @@ export const UserPanelContent = ({ riskScoreData={riskScoreState} queryId={USER_PANEL_RISK_SCORE_QUERY_ID} openDetailsPanel={openDetailsPanel} - entity={RiskScoreEntity.user} /> From beb285cd60388c6f1bf0f3ac7ee15a62974250e7 Mon Sep 17 00:00:00 2001 From: machadoum Date: Tue, 2 Jan 2024 12:10:10 +0100 Subject: [PATCH 07/13] Fix host panel content storybook --- .../flyout/entity_details/host_right/content.stories.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 index 17df523d4e21b..9bea5cb2a4ac2 100644 --- 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 @@ -36,9 +36,10 @@ storiesOf('Components/HostPanelContent', module) {}} /> )) .add('no observed data', () => ( @@ -57,9 +58,10 @@ storiesOf('Components/HostPanelContent', module) anomalies: { isLoading: false, anomalies: null, jobNameById: {} }, }} riskScoreState={riskScoreData} - contextID={'test-user-details'} + contextID={'test-host-details'} scopeId={'test-scopeId'} isDraggable={false} + openDetailsPanel={() => {}} /> )) .add('loading', () => ( @@ -78,8 +80,9 @@ storiesOf('Components/HostPanelContent', module) anomalies: { isLoading: true, anomalies: null, jobNameById: {} }, }} riskScoreState={riskScoreData} - contextID={'test-user-details'} + contextID={'test-host-details'} scopeId={'test-scopeId'} isDraggable={false} + openDetailsPanel={() => {}} /> )); From 6bed3c025401bed8b01feef18a975ed716cb648c Mon Sep 17 00:00:00 2001 From: machadoum Date: Tue, 2 Jan 2024 14:35:23 +0100 Subject: [PATCH 08/13] Fix small bugs and improve tests --- .../risk_summary_flyout/risk_summary.test.tsx | 78 +++++++++++++++++-- .../risk_summary_flyout/risk_summary.tsx | 15 ++-- .../flyout/entity_details/mocks/index.ts | 15 ---- .../entity_details/user_right/header.test.tsx | 2 +- .../components/host_overview/index.tsx | 1 + .../new_user_detail/managed_user.tsx | 4 +- .../managed_user_accordion.tsx | 8 +- 7 files changed, 90 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx index 2ac1f5a7512a8..1946c12aacdcf 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.test.tsx @@ -5,20 +5,38 @@ * 2.0. */ -import { mockRiskScoreState } from '../../../flyout/entity_details/mocks'; +import { + mockHostRiskScoreState, + mockUserRiskScoreState, +} from '../../../flyout/entity_details/mocks'; import { render } from '@testing-library/react'; import React from 'react'; import { TestProviders } from '../../../common/mock'; import { RiskSummary } from './risk_summary'; +import type { + LensAttributes, + VisualizationEmbeddableProps, +} from '../../../common/components/visualization_actions/types'; -jest.mock('../../../common/components/visualization_actions/visualization_embeddable'); +const mockVisualizationEmbeddable = jest + .fn() + .mockReturnValue(
); + +jest.mock('../../../common/components/visualization_actions/visualization_embeddable', () => ({ + VisualizationEmbeddable: (props: VisualizationEmbeddableProps) => + mockVisualizationEmbeddable(props), +})); describe('RiskSummary', () => { + beforeEach(() => { + mockVisualizationEmbeddable.mockClear(); + }); + it('renders risk summary table', () => { const { getByTestId } = render( {}} /> @@ -34,7 +52,7 @@ describe('RiskSummary', () => { const { getByTestId } = render( {}} /> @@ -47,7 +65,7 @@ describe('RiskSummary', () => { const { getByTestId } = render( {}} /> @@ -61,7 +79,7 @@ describe('RiskSummary', () => { const { getByTestId } = render( {}} /> @@ -70,4 +88,52 @@ describe('RiskSummary', () => { expect(getByTestId('risk-summary-updatedAt')).toHaveTextContent('Updated Nov 8, 1989'); }); + + it('builds lens attributes for host risk score', () => { + render( + + {}} + /> + + ); + + const lensAttributes: LensAttributes = + mockVisualizationEmbeddable.mock.calls[0][0].lensAttributes; + const datasourceLayers = Object.values(lensAttributes.state.datasourceStates.formBased.layers); + const firstColumn = Object.values(datasourceLayers[0].columns)[0]; + + expect(lensAttributes.state.query.query).toEqual('host.name: test'); + expect(firstColumn).toEqual( + expect.objectContaining({ + sourceField: 'host.risk.calculated_score_norm', + }) + ); + }); + + it('builds lens attributes for user risk score', () => { + render( + + {}} + /> + + ); + + const lensAttributes: LensAttributes = + mockVisualizationEmbeddable.mock.calls[0][0].lensAttributes; + const datasourceLayers = Object.values(lensAttributes.state.datasourceStates.formBased.layers); + const firstColumn = Object.values(datasourceLayers[0].columns)[0]; + + expect(lensAttributes.state.query.query).toEqual('user.name: test'); + expect(firstColumn).toEqual( + expect.objectContaining({ + sourceField: 'user.risk.calculated_score_norm', + }) + ); + }); }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx index cf5ce4c6281a2..aee1fcf2bcd04 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx @@ -48,8 +48,10 @@ interface TableItem { const LENS_VISUALIZATION_HEIGHT = 126; // Static height in pixels specified by design const LAST_30_DAYS = { from: 'now-30d', to: 'now' }; -function isUserRiskData(riskData: UserRiskScore | HostRiskScore): riskData is UserRiskScore { - return (riskData as UserRiskScore).user !== undefined; +function isUserRiskData( + riskData: UserRiskScore | HostRiskScore | undefined +): riskData is UserRiskScore { + return !!riskData && (riskData as UserRiskScore).user !== undefined; } const getEntityData = (riskData: UserRiskScore | HostRiskScore | undefined) => { @@ -69,13 +71,16 @@ const RiskSummaryComponent = ({ const { euiTheme } = useEuiTheme(); const lensAttributes = useMemo(() => { + const entityName = entityData?.name ?? ''; + const fieldName = isUserRiskData(riskData) ? 'user.name' : 'host.name'; + return getRiskScoreSummaryAttributes({ severity: entityData?.risk?.calculated_level, - query: `user.name: ${entityData?.name}`, + query: `${fieldName}: ${entityName}`, spaceId: 'default', - riskEntity: RiskScoreEntity.user, + riskEntity: isUserRiskData(riskData) ? RiskScoreEntity.user : RiskScoreEntity.host, }); - }, [entityData]); + }, [entityData?.name, entityData?.risk?.calculated_level, riskData]); const columns: Array> = useMemo( () => [ 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 index c66f16f5320e4..71804b0adaa16 100644 --- 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 @@ -98,21 +98,6 @@ export const mockHostRiskScoreState: RiskScoreState = { loading: false, }; -export const mockRiskScoreState = { - data: [hostRiskScore], - inspect: { - dsl: [], - response: [], - }, - isInspected: false, - refetch: () => {}, - totalCount: 0, - isModuleEnabled: true, - isAuthorized: true, - isDeprecated: false, - loading: false, -}; - const hostMetadata: HostMetadataInterface = { '@timestamp': 1036358673463478, diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/header.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/header.test.tsx index 861c9b2e2c1fb..fc815c980991c 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/header.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/header.test.tsx @@ -12,9 +12,9 @@ import { TestProviders } from '../../../common/mock'; import { managedUserDetails, mockManagedUserData, - mockObservedUser, } from '../../../timelines/components/side_panel/new_user_detail/__mocks__'; import { UserPanelHeader } from './header'; +import { mockObservedUser } from './mocks'; const mockProps = { userName: 'test', 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 9054605250808..a6409c587e0a6 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,6 +10,7 @@ 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/new_user_detail/managed_user.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/managed_user.tsx index 590f120b19687..635cf6a2868fd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/managed_user.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/managed_user.tsx @@ -18,7 +18,7 @@ import { import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/css'; -import type { UserDetailsLeftPanelTab } from '../../../../flyout/entity_details/user_details_left/tabs'; +import type { EntityDetailsLeftPanelTab } from '../../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; import { UserAssetTableType } from '../../../../explore/users/store/model'; import type { ManagedUserFields } from '../../../../../common/search_strategy/security_solution/users/managed_details'; import { ManagedUserDatasetKey } from '../../../../../common/search_strategy/security_solution/users/managed_details'; @@ -47,7 +47,7 @@ export const ManagedUser = ({ managedUser: ManagedUserData; contextID: string; isDraggable: boolean; - openDetailsPanel: (tab: UserDetailsLeftPanelTab) => void; + openDetailsPanel: (tab: EntityDetailsLeftPanelTab) => void; }) => { const entraManagedUser = managedUser.data?.[ManagedUserDatasetKey.ENTRA]; const oktaManagedUser = managedUser.data?.[ManagedUserDatasetKey.OKTA]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/managed_user_accordion.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/managed_user_accordion.tsx index a03775f61cf26..ad8b089adc168 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/managed_user_accordion.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/new_user_detail/managed_user_accordion.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; import { get } from 'lodash/fp'; -import { UserDetailsLeftPanelTab } from '../../../../flyout/entity_details/user_details_left/tabs'; +import { EntityDetailsLeftPanelTab } from '../../../../flyout/entity_details/shared/components/left_panel/left_panel_header'; import { ExpandablePanel } from '../../../../flyout/shared/components/expandable_panel'; import type { ManagedUserFields } from '../../../../../common/search_strategy/security_solution/users/managed_details'; @@ -23,7 +23,7 @@ interface ManagedUserAccordionProps { title: string; managedUser: ManagedUserFields; tableType: UserAssetTableType; - openDetailsPanel: (tab: UserDetailsLeftPanelTab) => void; + openDetailsPanel: (tab: EntityDetailsLeftPanelTab) => void; } export const ManagedUserAccordion: React.FC = ({ @@ -66,8 +66,8 @@ export const ManagedUserAccordion: React.FC = ({ callback: () => openDetailsPanel( tableType === UserAssetTableType.assetOkta - ? UserDetailsLeftPanelTab.OKTA - : UserDetailsLeftPanelTab.ENTRA + ? EntityDetailsLeftPanelTab.OKTA + : EntityDetailsLeftPanelTab.ENTRA ), tooltip: ( Date: Tue, 2 Jan 2024 17:00:31 +0000 Subject: [PATCH 09/13] [CI] Auto-commit changed files from 'node scripts/notice' --- NOTICE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NOTICE.txt b/NOTICE.txt index 45af6e5231783..d02031c4b5a2b 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,5 +1,5 @@ Kibana source code with Kibana X-Pack source code -Copyright 2012-2023 Elasticsearch B.V. +Copyright 2012-2024 Elasticsearch B.V. --- Pretty handling of logarithmic axes. From ed7804359ca2d63abefd1b78f1cb113aa1f977ec Mon Sep 17 00:00:00 2001 From: machadoum Date: Wed, 3 Jan 2024 09:19:22 +0100 Subject: [PATCH 10/13] Update translations --- x-pack/plugins/translations/translations/fr-FR.json | 9 --------- x-pack/plugins/translations/translations/ja-JP.json | 9 --------- x-pack/plugins/translations/translations/zh-CN.json | 9 --------- 3 files changed, 27 deletions(-) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 7c828eaee3afa..6b4fb501c715e 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -32024,7 +32024,6 @@ "xpack.securitySolution.timeline.properties.timelineToggleButtonAriaLabel": "{isOpen, select, false {Ouvrir} true {Fermer} other {Bascule}} la chronologie {title}", "xpack.securitySolution.timeline.saveTimeline.modal.warning.title": "Vous avez une {timeline} non enregistrée. Voulez-vous l'enregistrer ?", "xpack.securitySolution.timeline.searchBoxPlaceholder": "par ex. nom ou description de {timeline}", - "xpack.securitySolution.timeline.userDetails.observedUserUpdatedTime": "Mis à jour {time}", "xpack.securitySolution.timeline.userDetails.updatedTime": "Mis à jour {time}", "xpack.securitySolution.timeline.youAreInAnEventRendererScreenReaderOnly": "Vous êtes dans un outil de rendu d'événement pour la ligne : {row}. Appuyez sur la touche fléchée vers le haut pour quitter et revenir à la ligne en cours, ou sur la touche fléchée vers le bas pour quitter et passer à la ligne suivante.", "xpack.securitySolution.timeline.youAreInATableCellScreenReaderOnly": "Vous êtes dans une cellule de tableau. Ligne : {row}, colonne : {column}", @@ -36365,25 +36364,17 @@ "xpack.securitySolution.timeline.userDetails.addExternalIntegrationButton": "Ajouter des intégrations externes", "xpack.securitySolution.timeline.userDetails.closeButton": "fermer", "xpack.securitySolution.timeline.userDetails.failManagedUserDescription": "Impossible de lancer la recherche sur des données gérées par l'utilisateur", - "xpack.securitySolution.timeline.userDetails.familyLabel": "Famille", "xpack.securitySolution.timeline.userDetails.fieldColumnTitle": "Champ", - "xpack.securitySolution.timeline.userDetails.firstSeenLabel": "Vu en premier", - "xpack.securitySolution.timeline.userDetails.hostOsNameLabel": "Système d'exploitation", - "xpack.securitySolution.timeline.userDetails.ipAddressesLabel": "Adresses IP", - "xpack.securitySolution.timeline.userDetails.lastSeenLabel": "Vu en dernier", "xpack.securitySolution.timeline.userDetails.managedBadge": "GÉRÉ", "xpack.securitySolution.timeline.userDetails.managedDataTitle": "Données gérées", "xpack.securitySolution.timeline.userDetails.managedUserInspectTitle": "Géré par l'utilisateur", - "xpack.securitySolution.timeline.userDetails.maxAnomalyScoreByJobLabel": "Score maximal d'anomalie par tâche", "xpack.securitySolution.timeline.userDetails.noActiveIntegrationText": "Les intégrations externes peuvent fournir des métadonnées supplémentaires et vous aider à gérer les utilisateurs.", "xpack.securitySolution.timeline.userDetails.noActiveIntegrationTitle": "Vous n'avez aucune intégration active.", "xpack.securitySolution.timeline.userDetails.noAzureDataText": "Si vous vous attendiez à voir des métadonnées pour cet utilisateur, assurez-vous d'avoir correctement configuré vos intégrations.", "xpack.securitySolution.timeline.userDetails.noAzureDataTitle": "Métadonnées introuvables pour cet utilisateur", "xpack.securitySolution.timeline.userDetails.observedBadge": "OBSERVÉ", "xpack.securitySolution.timeline.userDetails.observedDataTitle": "Données observées", - "xpack.securitySolution.timeline.userDetails.observedUserInspectTitle": "Utilisateur observé", "xpack.securitySolution.timeline.userDetails.riskScoreLabel": "Score de risque", - "xpack.securitySolution.timeline.userDetails.userIdLabel": "ID utilisateur", "xpack.securitySolution.timeline.userDetails.userLabel": "Utilisateur", "xpack.securitySolution.timeline.userDetails.valuesColumnTitle": "Valeurs", "xpack.securitySolution.timelineEvents.errorSearchDescription": "Une erreur s'est produite lors de la recherche d'événements de la chronologie", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index af1f51321cb60..afc85771b6a38 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -32023,7 +32023,6 @@ "xpack.securitySolution.timeline.properties.timelineToggleButtonAriaLabel": "タイムライン\"{title}\"を{isOpen, select, false {開く} true {閉じる} other {切り替え}}", "xpack.securitySolution.timeline.saveTimeline.modal.warning.title": "保存されていない{timeline}があります。保存しますか?", "xpack.securitySolution.timeline.searchBoxPlaceholder": "例:{timeline}名または説明", - "xpack.securitySolution.timeline.userDetails.observedUserUpdatedTime": "{time}を更新しました", "xpack.securitySolution.timeline.userDetails.updatedTime": "{time}を更新しました", "xpack.securitySolution.timeline.youAreInAnEventRendererScreenReaderOnly": "行{row}のイベントレンダラーを表示しています。上矢印キーを押すと、終了して現在の行に戻ります。下矢印キーを押すと、終了して次の行に進みます。", "xpack.securitySolution.timeline.youAreInATableCellScreenReaderOnly": "表セルの行{row}、列{column}にいます", @@ -36364,25 +36363,17 @@ "xpack.securitySolution.timeline.userDetails.addExternalIntegrationButton": "外部統合を追加", "xpack.securitySolution.timeline.userDetails.closeButton": "閉じる", "xpack.securitySolution.timeline.userDetails.failManagedUserDescription": "ユーザーが管理するデータで検索を実行できませんでした", - "xpack.securitySolution.timeline.userDetails.familyLabel": "ファミリー", "xpack.securitySolution.timeline.userDetails.fieldColumnTitle": "フィールド", - "xpack.securitySolution.timeline.userDetails.firstSeenLabel": "初回の認識", - "xpack.securitySolution.timeline.userDetails.hostOsNameLabel": "オペレーティングシステム", - "xpack.securitySolution.timeline.userDetails.ipAddressesLabel": "IP アドレス", - "xpack.securitySolution.timeline.userDetails.lastSeenLabel": "前回の認識", "xpack.securitySolution.timeline.userDetails.managedBadge": "管理対象", "xpack.securitySolution.timeline.userDetails.managedDataTitle": "管理対象のデータ", "xpack.securitySolution.timeline.userDetails.managedUserInspectTitle": "管理対象のユーザー", - "xpack.securitySolution.timeline.userDetails.maxAnomalyScoreByJobLabel": "ジョブ別の最高異常スコア", "xpack.securitySolution.timeline.userDetails.noActiveIntegrationText": "外部統合は追加のメタデータを提供し、ユーザーの管理を支援できます。", "xpack.securitySolution.timeline.userDetails.noActiveIntegrationTitle": "アクティブな統合がありません", "xpack.securitySolution.timeline.userDetails.noAzureDataText": "このユーザーのメタデータが表示されることが想定される場合は、統合を正しく構成したことを確認してください。", "xpack.securitySolution.timeline.userDetails.noAzureDataTitle": "このユーザーのメタデータが見つかりません", "xpack.securitySolution.timeline.userDetails.observedBadge": "観測済み", "xpack.securitySolution.timeline.userDetails.observedDataTitle": "観測されたデータ", - "xpack.securitySolution.timeline.userDetails.observedUserInspectTitle": "観測されたユーザー", "xpack.securitySolution.timeline.userDetails.riskScoreLabel": "リスクスコア", - "xpack.securitySolution.timeline.userDetails.userIdLabel": "ユーザーID", "xpack.securitySolution.timeline.userDetails.userLabel": "ユーザー", "xpack.securitySolution.timeline.userDetails.valuesColumnTitle": "値", "xpack.securitySolution.timelineEvents.errorSearchDescription": "タイムラインイベント検索でエラーが発生しました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 53b6da3e5fd08..de28bc7c718bb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -32018,7 +32018,6 @@ "xpack.securitySolution.timeline.properties.timelineToggleButtonAriaLabel": "{isOpen, select, false {打开} true {关闭} other {切换}}时间线 {title}", "xpack.securitySolution.timeline.saveTimeline.modal.warning.title": "您的 {timeline} 未保存。是否保存?", "xpack.securitySolution.timeline.searchBoxPlaceholder": "例如 {timeline} 名称或描述", - "xpack.securitySolution.timeline.userDetails.observedUserUpdatedTime": "已更新 {time}", "xpack.securitySolution.timeline.userDetails.updatedTime": "已更新 {time}", "xpack.securitySolution.timeline.youAreInAnEventRendererScreenReaderOnly": "您正处于第 {row} 行的事件呈现器中。按向上箭头键退出并返回当前行,或按向下箭头键退出并前进到下一行。", "xpack.securitySolution.timeline.youAreInATableCellScreenReaderOnly": "您处在表单元格中。行:{row},列:{column}", @@ -36359,25 +36358,17 @@ "xpack.securitySolution.timeline.userDetails.addExternalIntegrationButton": "添加外部集成", "xpack.securitySolution.timeline.userDetails.closeButton": "关闭", "xpack.securitySolution.timeline.userDetails.failManagedUserDescription": "无法对用户托管数据执行搜索", - "xpack.securitySolution.timeline.userDetails.familyLabel": "系列", "xpack.securitySolution.timeline.userDetails.fieldColumnTitle": "字段", - "xpack.securitySolution.timeline.userDetails.firstSeenLabel": "首次看到时间", - "xpack.securitySolution.timeline.userDetails.hostOsNameLabel": "操作系统", - "xpack.securitySolution.timeline.userDetails.ipAddressesLabel": "IP 地址", - "xpack.securitySolution.timeline.userDetails.lastSeenLabel": "最后看到时间", "xpack.securitySolution.timeline.userDetails.managedBadge": "托管", "xpack.securitySolution.timeline.userDetails.managedDataTitle": "托管数据", "xpack.securitySolution.timeline.userDetails.managedUserInspectTitle": "托管用户", - "xpack.securitySolution.timeline.userDetails.maxAnomalyScoreByJobLabel": "最大异常分数(按作业)", "xpack.securitySolution.timeline.userDetails.noActiveIntegrationText": "外部集成可提供其他元数据并帮助您管理用户。", "xpack.securitySolution.timeline.userDetails.noActiveIntegrationTitle": "您没有任何活动集成", "xpack.securitySolution.timeline.userDetails.noAzureDataText": "如果计划查看此用户的元数据,请确保已正确配置集成。", "xpack.securitySolution.timeline.userDetails.noAzureDataTitle": "找不到此用户的元数据", "xpack.securitySolution.timeline.userDetails.observedBadge": "已观察", "xpack.securitySolution.timeline.userDetails.observedDataTitle": "观察数据", - "xpack.securitySolution.timeline.userDetails.observedUserInspectTitle": "已观察用户", "xpack.securitySolution.timeline.userDetails.riskScoreLabel": "风险分数", - "xpack.securitySolution.timeline.userDetails.userIdLabel": "用户 ID", "xpack.securitySolution.timeline.userDetails.userLabel": "用户", "xpack.securitySolution.timeline.userDetails.valuesColumnTitle": "值", "xpack.securitySolution.timelineEvents.errorSearchDescription": "搜索时间线事件时发生错误", From e89111d6efdfdd1e09851f5116c7a2b28a326e27 Mon Sep 17 00:00:00 2001 From: machadoum Date: Thu, 4 Jan 2024 09:48:51 +0100 Subject: [PATCH 11/13] Imrpove code and comments --- .../plugins/security_solution/common/utility_types.ts | 4 ++++ .../components/risk_summary_flyout/risk_summary.tsx | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/common/utility_types.ts b/x-pack/plugins/security_solution/common/utility_types.ts index 4db1808e368af..aaf1dfafb6575 100644 --- a/x-pack/plugins/security_solution/common/utility_types.ts +++ b/x-pack/plugins/security_solution/common/utility_types.ts @@ -73,4 +73,8 @@ export const assertUnreachable = ( }; type Without = { [P in Exclude]?: never }; +/** + * The XOR (exclusive OR) allows to ensure that a variable conforms to only one of several possible types. + * Read more: https://medium.com/@aeron169/building-a-xor-type-in-typescript-5f4f7e709a9d + */ 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/risk_summary_flyout/risk_summary.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx index aee1fcf2bcd04..c8d4f093fdc4d 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_flyout/risk_summary.tsx @@ -55,8 +55,14 @@ function isUserRiskData( } const getEntityData = (riskData: UserRiskScore | HostRiskScore | undefined) => { - if (!riskData) return; - if (isUserRiskData(riskData)) return riskData.user; + if (!riskData) { + return; + } + + if (isUserRiskData(riskData)) { + return riskData.user; + } + return riskData.host; }; From b9725ea7d2b1e47a2c9e46c102567ea7481a4d50 Mon Sep 17 00:00:00 2001 From: machadoum Date: Thu, 4 Jan 2024 14:18:32 +0100 Subject: [PATCH 12/13] Please code review --- .../fields/endpoint_policy_fields.tsx | 14 ++++++-------- .../host_right/hooks/use_observed_host.ts | 2 +- .../hooks/use_observed_host_fields.ts | 17 ++++++++--------- .../shared/components/entity_table/columns.tsx | 4 +++- .../user_right/hooks/use_observed_user.ts | 2 +- .../user_right/hooks/use_observed_user_items.ts | 4 +++- .../timeline/body/renderers/host_name.tsx | 6 +++--- 7 files changed, 25 insertions(+), 24 deletions(-) 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 index 125534c785dd7..fd9b8c744a7b2 100644 --- 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 @@ -29,15 +29,13 @@ export const policyFields: EntityTableRows> = [ label: i18n.POLICY_STATUS, render: (hostData: ObservedEntityData) => { const appliedPolicy = hostData.details.endpoint?.hostInfo?.metadata.Endpoint.policy.applied; + const policyColor = + appliedPolicy?.status === HostPolicyResponseActionStatus.failure + ? 'danger' + : appliedPolicy?.status; + return appliedPolicy?.status ? ( - + {appliedPolicy?.status} ) : ( 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 index f424fcfe44bb9..980407b034649 100644 --- 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 @@ -59,7 +59,7 @@ export const useObservedHost = ( return useMemo( () => ({ details: hostDetails, - isLoading: isLoading && loadingLastSeen && loadingFirstSeen, + isLoading: isLoading || loadingLastSeen || loadingFirstSeen, firstSeen: { date: firstSeen, isLoading: loadingFirstSeen, 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 index 67cd23256068f..255bb54c2c58a 100644 --- 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 @@ -20,17 +20,16 @@ export const useObservedHostFields = ( ): EntityTableRows> => { const mlCapabilities = useMlCapabilities(); - const fields: EntityTableRows> = useMemo( - () => [ + return useMemo(() => { + if (hostData == null) { + return []; + } + + return [ ...basicHostFields, ...getAnomaliesFields(mlCapabilities), ...cloudFields, ...policyFields, - ], - [mlCapabilities] - ); - - if (!hostData.details) return []; - - return fields; + ]; + }, [hostData, mlCapabilities]); }; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/columns.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/columns.tsx index bd0891c962d1e..e97ab9b4accae 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/entity_table/columns.tsx @@ -65,7 +65,9 @@ export const getEntityTableColumns = ( ); } - if (render) return render(data); + if (render) { + return render(data); + } return getEmptyTagValue(); }, diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user.ts b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user.ts index 8d7476124efd0..6d1ae0ab11e00 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user.ts +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/hooks/use_observed_user.ts @@ -58,7 +58,7 @@ export const useObservedUser = ( return useMemo( () => ({ details: observedUserDetails, - isLoading: loadingObservedUser && loadingLastSeen && loadingFirstSeen, + isLoading: loadingObservedUser || loadingLastSeen || loadingFirstSeen, firstSeen: { date: firstSeen, isLoading: loadingFirstSeen, 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 index a70da4214a8ee..7275b2ca55570 100644 --- 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 @@ -63,7 +63,9 @@ export const useObservedUserItems = ( [mlCapabilities] ); - if (!userData.details) return []; + if (!userData.details) { + return []; + } return fields; }; 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 2f343373a4616..c6b8d4f2d4cd3 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 @@ -50,7 +50,7 @@ const HostNameComponent: React.FC = ({ title, value, }) => { - const isNewHostDetailsFlyoutEnable = useIsExperimentalFeatureEnabled('newHostDetailsFlyout'); + const isNewHostDetailsFlyoutEnabled = useIsExperimentalFeatureEnabled('newHostDetailsFlyout'); const { openRightPanel } = useExpandableFlyoutContext(); const dispatch = useDispatch(); @@ -69,7 +69,7 @@ const HostNameComponent: React.FC = ({ if (eventContext && isInTimelineContext) { const { timelineID, tabType } = eventContext; - if (isNewHostDetailsFlyoutEnable && !isTimelineScope(timelineID)) { + if (isNewHostDetailsFlyoutEnabled && !isTimelineScope(timelineID)) { openRightPanel({ id: HostPanelKey, params: { @@ -107,7 +107,7 @@ const HostNameComponent: React.FC = ({ onClick, eventContext, isInTimelineContext, - isNewHostDetailsFlyoutEnable, + isNewHostDetailsFlyoutEnabled, openRightPanel, hostName, contextId, From 79bdc7bc52ded08ec009208cde8698900fd86523 Mon Sep 17 00:00:00 2001 From: machadoum Date: Thu, 4 Jan 2024 14:49:01 +0100 Subject: [PATCH 13/13] Update mock risk score timestamp fields to formatted string ES returns formatted strings by default. --- .../public/flyout/entity_details/mocks/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index 71804b0adaa16..01dafb9d6b47a 100644 --- 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 @@ -19,7 +19,7 @@ import { RiskCategories } from '../../../../common/entity_analytics/risk_engine' import type { ObservedEntityData } from '../shared/components/observed_entity/types'; const userRiskScore: UserRiskScore = { - '@timestamp': '626569200000', + '@timestamp': '1989-11-08T23:00:00.000Z', user: { name: 'test', risk: { @@ -40,11 +40,11 @@ const userRiskScore: UserRiskScore = { }, }, alertsCount: 0, - oldestAlertTimestamp: '626569200000', + oldestAlertTimestamp: '1989-11-08T23:00:00.000Z', }; const hostRiskScore: HostRiskScore = { - '@timestamp': '626569200000', + '@timestamp': '1989-11-08T23:00:00.000Z', host: { name: 'test', risk: { @@ -65,7 +65,7 @@ const hostRiskScore: HostRiskScore = { }, }, alertsCount: 0, - oldestAlertTimestamp: '626569200000', + oldestAlertTimestamp: '1989-11-08T23:00:00.000Z', }; export const mockUserRiskScoreState: RiskScoreState = {