Skip to content

Commit

Permalink
Create new host details flyout
Browse files Browse the repository at this point in the history
* Refactor user flyout components to be reused by hosts
  • Loading branch information
machadoum committed Dec 21, 2023
1 parent 68883b3 commit 32a45ba
Show file tree
Hide file tree
Showing 53 changed files with 1,990 additions and 565 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/security_solution/common/utility_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,6 @@ export const assertUnreachable = (
): never => {
throw new Error(`${message}: ${x}`);
};

type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
export type XOR<T, U> = T | U extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
<FormattedMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const useHostDetails = ({
id = ID,
skip = false,
startDate,
}: UseHostDetails): [boolean, HostDetailsArgs] => {
}: UseHostDetails): [boolean, HostDetailsArgs, inputsModel.Refetch] => {
const {
loading,
result: response,
Expand Down Expand Up @@ -91,5 +91,5 @@ export const useHostDetails = ({
}
}, [hostDetailsRequest, search, skip]);

return [loading, hostDetailsResponse];
return [loading, hostDetailsResponse, refetch];
};
Original file line number Diff line number Diff line change
@@ -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<string, unknown> {
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 (
<>
<LeftPanelHeader
selectedTabId={selectedTabId}
setSelectedTabId={setSelectedTabId}
tabs={tabs}
/>
<LeftPanelContent selectedTabId={selectedTabId} tabs={tabs} />
</>
);
};

HostDetailsPanel.displayName = 'HostDetailsPanel';
Original file line number Diff line number Diff line change
@@ -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) => (
<StorybookProviders>
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<EuiFlyout size="m" onClose={() => {}}>
{storyFn()}
</EuiFlyout>
</ExpandableFlyoutContext.Provider>
</StorybookProviders>
))
.add('default', () => (
<UserPanelContent
managedUser={mockManagedUserData}
observedUser={mockObservedUser}
riskScoreState={riskScoreData}
contextID={'test-user-details'}
scopeId={'test-scopeId'}
isDraggable={false}
/>
))
.add('integration disabled', () => (
<UserPanelContent
managedUser={{
data: undefined,
isLoading: false,
isIntegrationEnabled: false,
}}
observedUser={mockObservedUser}
riskScoreState={riskScoreData}
contextID={'test-user-details'}
scopeId={'test-scopeId'}
isDraggable={false}
/>
))
.add('no managed data', () => (
<UserPanelContent
managedUser={{
data: undefined,
isLoading: false,
isIntegrationEnabled: true,
}}
observedUser={mockObservedUser}
riskScoreState={riskScoreData}
contextID={'test-user-details'}
scopeId={'test-scopeId'}
isDraggable={false}
/>
))
.add('no observed data', () => (
<UserPanelContent
managedUser={mockManagedUserData}
observedUser={{
details: {
user: {
id: [],
domain: [],
},
host: {
ip: [],
os: {
name: [],
family: [],
},
},
},
isLoading: false,
firstSeen: {
isLoading: false,
date: undefined,
},
lastSeen: {
isLoading: false,
date: undefined,
},
anomalies: { isLoading: false, anomalies: null, jobNameById: {} },
}}
riskScoreState={riskScoreData}
contextID={'test-user-details'}
scopeId={'test-scopeId'}
isDraggable={false}
/>
))
.add('loading', () => (
<UserPanelContent
managedUser={{
data: undefined,
isLoading: true,
isIntegrationEnabled: true,
}}
observedUser={{
details: {
user: {
id: [],
domain: [],
},
host: {
ip: [],
os: {
name: [],
family: [],
},
},
},
isLoading: true,
firstSeen: {
isLoading: true,
date: undefined,
},
lastSeen: {
isLoading: true,
date: undefined,
},
anomalies: { isLoading: true, anomalies: null, jobNameById: {} },
}}
riskScoreState={riskScoreData}
contextID={'test-user-details'}
scopeId={'test-scopeId'}
isDraggable={false}
/>
));
Original file line number Diff line number Diff line change
@@ -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<HostItem>;
riskScoreState: RiskScoreState<RiskScoreEntity.user>;
contextID: string;
scopeId: string;
isDraggable: boolean;
}

export const HostPanelContent = ({
observedHost,
riskScoreState,
contextID,
scopeId,
isDraggable,
}: HostPanelContentProps) => {
const observedFields = useObservedHostFields(observedHost);

return (
<FlyoutBody>
{riskScoreState.isModuleEnabled && riskScoreState.data?.length !== 0 && (
<>
{/* TODO <RiskSummary riskScoreData={riskScoreState} queryId={HOST_PANEL_RISK_SCORE_QUERY_ID} /> */}
<EuiHorizontalRule margin="m" />
</>
)}
<ObservedEntity
observedData={observedHost}
contextID={contextID}
scopeId={scopeId}
isDraggable={isDraggable}
observedFields={observedFields}
queryId={HOST_PANEL_OBSERVED_HOST_QUERY_ID}
/>
</FlyoutBody>
);
};
Original file line number Diff line number Diff line change
@@ -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<ObservedEntityData<HostItem>> = [
{
label: i18n.HOST_ID,
getValues: (hostData: ObservedEntityData<HostItem>) => hostData.details.host?.id,
field: 'host.id',
},
{
label: i18n.FIRST_SEEN,
render: (hostData: ObservedEntityData<HostItem>) =>
hostData.firstSeen.date ? (
<FormattedRelativePreferenceDate value={hostData.firstSeen.date} />
) : (
getEmptyTagValue()
),
},
{
label: i18n.LAST_SEEN,
render: (hostData: ObservedEntityData<HostItem>) =>
hostData.lastSeen.date ? (
<FormattedRelativePreferenceDate value={hostData.lastSeen.date} />
) : (
getEmptyTagValue()
),
},
{
label: i18n.IP_ADDRESSES,
field: 'host.ip',
getValues: (hostData: ObservedEntityData<HostItem>) => hostData.details.host?.ip,
renderField: (ip: string) => {
return <NetworkDetailsLink ip={ip} />;
},
},
{
label: i18n.MAC_ADDRESSES,
getValues: (hostData: ObservedEntityData<HostItem>) => hostData.details.host?.mac,
field: 'host.mac',
},
{
label: i18n.PLATFORM,
getValues: (hostData: ObservedEntityData<HostItem>) => hostData.details.host?.os?.platform,
field: 'host.os.platform',
},
{
label: i18n.OS,
getValues: (hostData: ObservedEntityData<HostItem>) => hostData.details.host?.os?.name,
field: 'host.os.name',
},
{
label: i18n.FAMILY,
getValues: (hostData: ObservedEntityData<HostItem>) => hostData.details.host?.os?.family,
field: 'host.os.family',
},
{
label: i18n.VERSION,
getValues: (hostData: ObservedEntityData<HostItem>) => hostData.details.host?.os?.version,
field: 'host.os.version',
},
{
label: i18n.ARCHITECTURE,
getValues: (hostData: ObservedEntityData<HostItem>) => hostData.details.host?.architecture,
field: 'host.architecture',
},
];
Loading

0 comments on commit 32a45ba

Please sign in to comment.