mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Document details flyout - update insight KPI count (#196617)
## Summary This PR made some updates to the insights KPI following https://github.com/elastic/kibana/pull/195509 - Updated all the counts to be total alerts/misconfigurations/vulnerabilities - Clicking on the count badge opens timeline (alerts) or entity preview - Revert the order of the distribution bar for alerts to align with others https://github.com/user-attachments/assets/6d65503a-26b1-4db4-9118-a63ad66ac7b6 Latest design  ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
parent
8dd895fe57
commit
71951416ca
9 changed files with 241 additions and 52 deletions
|
@ -8,7 +8,7 @@
|
|||
import type { FC, PropsWithChildren } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
|
||||
import type { IconType } from '@elastic/eui';
|
||||
import type { IconType, EuiButtonEmptyProps } from '@elastic/eui';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
|
@ -34,6 +34,7 @@ export interface InvestigateInTimelineButtonProps {
|
|||
isDisabled?: boolean;
|
||||
iconType?: IconType;
|
||||
children?: React.ReactNode;
|
||||
flush?: EuiButtonEmptyProps['flush'];
|
||||
}
|
||||
|
||||
export const InvestigateInTimelineButton: FC<
|
||||
|
@ -46,6 +47,7 @@ export const InvestigateInTimelineButton: FC<
|
|||
timeRange,
|
||||
keepDataView,
|
||||
iconType,
|
||||
flush,
|
||||
...rest
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -118,7 +120,7 @@ export const InvestigateInTimelineButton: FC<
|
|||
<EuiButtonEmpty
|
||||
aria-label={ACTION_INVESTIGATE_IN_TIMELINE}
|
||||
onClick={configureAndOpenTimeline}
|
||||
flush="right"
|
||||
flush={flush ?? 'right'}
|
||||
size="xs"
|
||||
iconType={iconType}
|
||||
>
|
||||
|
|
|
@ -48,6 +48,7 @@ describe('AlertCountInsight', () => {
|
|||
const { getByTestId } = renderAlertCountInsight();
|
||||
expect(getByTestId(testId)).toBeInTheDocument();
|
||||
expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument();
|
||||
expect(getByTestId(`${testId}-count`)).toHaveTextContent('177');
|
||||
});
|
||||
|
||||
it('renders loading spinner if data is being fetched', () => {
|
||||
|
|
|
@ -16,8 +16,12 @@ import {
|
|||
getIsAlertsBySeverityData,
|
||||
getSeverityColor,
|
||||
} from '../../../../detections/components/alerts_kpis/severity_level_panel/helpers';
|
||||
import { FormattedCount } from '../../../../common/components/formatted_number';
|
||||
import { InvestigateInTimelineButton } from '../../../../common/components/event_details/investigate_in_timeline_button';
|
||||
import { getDataProvider } from '../../../../common/components/event_details/use_action_cell_data_provider';
|
||||
|
||||
const ENTITY_ALERT_COUNT_ID = 'entity-alert-count';
|
||||
const SEVERITIES = ['unknown', 'low', 'medium', 'high', 'critical'];
|
||||
|
||||
interface AlertCountInsightProps {
|
||||
/**
|
||||
|
@ -39,7 +43,7 @@ interface AlertCountInsightProps {
|
|||
}
|
||||
|
||||
/*
|
||||
* Displays a distribution bar with the count of critical alerts for a given entity
|
||||
* Displays a distribution bar with the total alert count for a given entity
|
||||
*/
|
||||
export const AlertCountInsight: React.FC<AlertCountInsightProps> = ({
|
||||
name,
|
||||
|
@ -56,22 +60,27 @@ export const AlertCountInsight: React.FC<AlertCountInsightProps> = ({
|
|||
uniqueQueryId,
|
||||
signalIndexName: null,
|
||||
});
|
||||
const dataProviders = useMemo(
|
||||
() => [getDataProvider(fieldName, `timeline-indicator-${fieldName}-${name}`, name)],
|
||||
[fieldName, name]
|
||||
);
|
||||
|
||||
const data = useMemo(() => (getIsAlertsBySeverityData(items) ? items : []), [items]);
|
||||
|
||||
const alertStats = useMemo(() => {
|
||||
return data.map((item) => ({
|
||||
key: item.key,
|
||||
count: item.value,
|
||||
color: getSeverityColor(item.key),
|
||||
}));
|
||||
}, [data]);
|
||||
|
||||
const count = useMemo(
|
||||
() => data.filter((item) => item.key === 'critical')[0]?.value ?? 0,
|
||||
const alertStats = useMemo(
|
||||
() =>
|
||||
data
|
||||
.map((item) => ({
|
||||
key: item.key,
|
||||
count: item.value,
|
||||
color: getSeverityColor(item.key),
|
||||
}))
|
||||
.sort((a, b) => SEVERITIES.indexOf(a.key) - SEVERITIES.indexOf(b.key)),
|
||||
[data]
|
||||
);
|
||||
|
||||
const totalAlertCount = useMemo(() => data.reduce((acc, item) => acc + item.value, 0), [data]);
|
||||
|
||||
if (!isLoading && items.length === 0) return null;
|
||||
|
||||
return (
|
||||
|
@ -87,7 +96,17 @@ export const AlertCountInsight: React.FC<AlertCountInsightProps> = ({
|
|||
/>
|
||||
}
|
||||
stats={alertStats}
|
||||
count={count}
|
||||
count={
|
||||
<div data-test-subj={`${dataTestSubj}-count`}>
|
||||
<InvestigateInTimelineButton
|
||||
asEmptyButton={true}
|
||||
dataProviders={dataProviders}
|
||||
flush={'both'}
|
||||
>
|
||||
<FormattedCount count={totalAlertCount} />
|
||||
</InvestigateInTimelineButton>
|
||||
</div>
|
||||
}
|
||||
direction={direction}
|
||||
data-test-subj={`${dataTestSubj}-distribution-bar`}
|
||||
/>
|
||||
|
|
|
@ -11,7 +11,7 @@ import { InsightDistributionBar } from './insight_distribution_bar';
|
|||
import { TestProviders } from '../../../../common/mock';
|
||||
|
||||
const title = 'test title';
|
||||
const count = 10;
|
||||
const count = <div data-test-subj="test-count">{'100'}</div>;
|
||||
const testId = 'test-id';
|
||||
const stats = [
|
||||
{
|
||||
|
@ -35,7 +35,7 @@ describe('<InsightDistributionBar />', () => {
|
|||
);
|
||||
expect(getByTestId(testId)).toBeInTheDocument();
|
||||
expect(getByText(title)).toBeInTheDocument();
|
||||
expect(getByTestId(`${testId}-badge`)).toHaveTextContent(`${count}`);
|
||||
expect(getByTestId('test-count')).toBeInTheDocument();
|
||||
expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
|
@ -17,7 +17,6 @@ import {
|
|||
type EuiFlexGroupProps,
|
||||
} from '@elastic/eui';
|
||||
import { DistributionBar } from '@kbn/security-solution-distribution-bar';
|
||||
import { FormattedCount } from '../../../../common/components/formatted_number';
|
||||
|
||||
export interface InsightDistributionBarProps {
|
||||
/**
|
||||
|
@ -31,7 +30,7 @@ export interface InsightDistributionBarProps {
|
|||
/**
|
||||
* Count to be displayed in the badge
|
||||
*/
|
||||
count: number;
|
||||
count: React.ReactNode;
|
||||
/**
|
||||
* Flex direction of the component
|
||||
*/
|
||||
|
@ -53,34 +52,53 @@ export const InsightDistributionBar: React.FC<InsightDistributionBarProps> = ({
|
|||
const { euiTheme } = useEuiTheme();
|
||||
const xsFontSize = useEuiFontSize('xs').fontSize;
|
||||
|
||||
const barComponent = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup gutterSize="xs" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<DistributionBar
|
||||
stats={stats}
|
||||
hideLastTooltip
|
||||
data-test-subj={`${dataTestSubj}-distribution-bar`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} data-test-subj={`${dataTestSubj}-badge`}>
|
||||
<EuiBadge color="hollow">{count}</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[stats, count, dataTestSubj]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction={direction} data-test-subj={dataTestSubj} responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction={direction}
|
||||
data-test-subj={dataTestSubj}
|
||||
responsive={false}
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
css={css`
|
||||
font-size: ${xsFontSize};
|
||||
font-weight: ${euiTheme.font.weight.bold};
|
||||
min-width: 115px;
|
||||
`}
|
||||
>
|
||||
{title}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="xs" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<DistributionBar
|
||||
stats={stats}
|
||||
hideLastTooltip
|
||||
data-test-subj={`${dataTestSubj}-distribution-bar`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} data-test-subj={`${dataTestSubj}-badge`}>
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedCount count={count} />
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{direction === 'column' ? (
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
margin-top: -${euiTheme.size.base};
|
||||
`}
|
||||
>
|
||||
{barComponent}
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<EuiFlexItem>{barComponent}</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,34 +10,87 @@ import { render } from '@testing-library/react';
|
|||
import { TestProviders } from '../../../../common/mock';
|
||||
import { MisconfigurationsInsight } from './misconfiguration_insight';
|
||||
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
|
||||
import { DocumentDetailsContext } from '../context';
|
||||
import { mockFlyoutApi } from '../mocks/mock_flyout_context';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { mockContextValue } from '../mocks/mock_context';
|
||||
import { HostPreviewPanelKey } from '../../../entity_details/host_right';
|
||||
import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview';
|
||||
import { UserPreviewPanelKey } from '../../../entity_details/user_right';
|
||||
import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview';
|
||||
|
||||
jest.mock('@kbn/expandable-flyout');
|
||||
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
|
||||
|
||||
const fieldName = 'host.name';
|
||||
const name = 'test host';
|
||||
const hostName = 'test host';
|
||||
const userName = 'test user';
|
||||
const testId = 'test';
|
||||
|
||||
const renderMisconfigurationsInsight = () => {
|
||||
const renderMisconfigurationsInsight = (fieldName: 'host.name' | 'user.name', value: string) => {
|
||||
return render(
|
||||
<TestProviders>
|
||||
<MisconfigurationsInsight name={name} fieldName={fieldName} data-test-subj={testId} />
|
||||
<DocumentDetailsContext.Provider value={mockContextValue}>
|
||||
<MisconfigurationsInsight name={value} fieldName={fieldName} data-test-subj={testId} />
|
||||
</DocumentDetailsContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
};
|
||||
|
||||
describe('MisconfigurationsInsight', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
(useMisconfigurationPreview as jest.Mock).mockReturnValue({
|
||||
data: { count: { passed: 1, failed: 2 } },
|
||||
});
|
||||
const { getByTestId } = renderMisconfigurationsInsight();
|
||||
const { getByTestId } = renderMisconfigurationsInsight('host.name', hostName);
|
||||
expect(getByTestId(testId)).toBeInTheDocument();
|
||||
expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders null if no misconfiguration data found', () => {
|
||||
(useMisconfigurationPreview as jest.Mock).mockReturnValue({});
|
||||
const { container } = renderMisconfigurationsInsight();
|
||||
const { container } = renderMisconfigurationsInsight('host.name', hostName);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
describe('should open entity flyout when clicking on badge', () => {
|
||||
it('should open host entity flyout when clicking on host badge', () => {
|
||||
(useMisconfigurationPreview as jest.Mock).mockReturnValue({
|
||||
data: { count: { passed: 1, failed: 2 } },
|
||||
});
|
||||
const { getByTestId } = renderMisconfigurationsInsight('host.name', hostName);
|
||||
expect(getByTestId(`${testId}-count`)).toHaveTextContent('3');
|
||||
|
||||
getByTestId(`${testId}-count`).click();
|
||||
expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({
|
||||
id: HostPreviewPanelKey,
|
||||
params: {
|
||||
hostName,
|
||||
banner: HOST_PREVIEW_BANNER,
|
||||
scopeId: mockContextValue.scopeId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should open user entity flyout when clicking on user badge', () => {
|
||||
(useMisconfigurationPreview as jest.Mock).mockReturnValue({
|
||||
data: { count: { passed: 2, failed: 3 } },
|
||||
});
|
||||
const { getByTestId } = renderMisconfigurationsInsight('user.name', userName);
|
||||
expect(getByTestId(`${testId}-count`)).toHaveTextContent('5');
|
||||
|
||||
getByTestId(`${testId}-count`).click();
|
||||
expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({
|
||||
id: UserPreviewPanelKey,
|
||||
params: {
|
||||
userName,
|
||||
banner: USER_PREVIEW_BANNER,
|
||||
scopeId: mockContextValue.scopeId,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,12 +6,16 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui';
|
||||
import { EuiFlexItem, type EuiFlexGroupProps, useEuiTheme } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { css } from '@emotion/css';
|
||||
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
|
||||
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
|
||||
import { InsightDistributionBar } from './insight_distribution_bar';
|
||||
import { getFindingsStats } from '../../../../cloud_security_posture/components/misconfiguration/misconfiguration_preview';
|
||||
import { FormattedCount } from '../../../../common/components/formatted_number';
|
||||
import { PreviewLink } from '../../../shared/components/preview_link';
|
||||
import { useDocumentDetailsContext } from '../context';
|
||||
|
||||
interface MisconfigurationsInsightProps {
|
||||
/**
|
||||
|
@ -33,7 +37,7 @@ interface MisconfigurationsInsightProps {
|
|||
}
|
||||
|
||||
/*
|
||||
* Displays a distribution bar with the count of failed misconfigurations for a given entity
|
||||
* Displays a distribution bar with the count of total misconfigurations for a given entity
|
||||
*/
|
||||
export const MisconfigurationsInsight: React.FC<MisconfigurationsInsightProps> = ({
|
||||
name,
|
||||
|
@ -41,6 +45,8 @@ export const MisconfigurationsInsight: React.FC<MisconfigurationsInsightProps> =
|
|||
direction,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}) => {
|
||||
const { scopeId, isPreview } = useDocumentDetailsContext();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { data } = useMisconfigurationPreview({
|
||||
query: buildEntityFlyoutPreviewQuery(fieldName, name),
|
||||
sort: [],
|
||||
|
@ -50,13 +56,39 @@ export const MisconfigurationsInsight: React.FC<MisconfigurationsInsightProps> =
|
|||
|
||||
const passedFindings = data?.count.passed || 0;
|
||||
const failedFindings = data?.count.failed || 0;
|
||||
const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0;
|
||||
const totalFindings = useMemo(
|
||||
() => passedFindings + failedFindings,
|
||||
[passedFindings, failedFindings]
|
||||
);
|
||||
const hasMisconfigurationFindings = totalFindings > 0;
|
||||
|
||||
const misconfigurationsStats = useMemo(
|
||||
() => getFindingsStats(passedFindings, failedFindings),
|
||||
[passedFindings, failedFindings]
|
||||
);
|
||||
|
||||
const count = useMemo(
|
||||
() => (
|
||||
<div
|
||||
css={css`
|
||||
margin-top: ${euiTheme.size.xs};
|
||||
margin-bottom: ${euiTheme.size.xs};
|
||||
`}
|
||||
>
|
||||
<PreviewLink
|
||||
field={fieldName}
|
||||
value={name}
|
||||
scopeId={scopeId}
|
||||
isPreview={isPreview}
|
||||
data-test-subj={`${dataTestSubj}-count`}
|
||||
>
|
||||
<FormattedCount count={totalFindings} />
|
||||
</PreviewLink>
|
||||
</div>
|
||||
),
|
||||
[totalFindings, fieldName, name, scopeId, isPreview, dataTestSubj, euiTheme.size]
|
||||
);
|
||||
|
||||
if (!hasMisconfigurationFindings) return null;
|
||||
|
||||
return (
|
||||
|
@ -69,7 +101,7 @@ export const MisconfigurationsInsight: React.FC<MisconfigurationsInsightProps> =
|
|||
/>
|
||||
}
|
||||
stats={misconfigurationsStats}
|
||||
count={failedFindings}
|
||||
count={count}
|
||||
direction={direction}
|
||||
data-test-subj={`${dataTestSubj}-distribution-bar`}
|
||||
/>
|
||||
|
|
|
@ -10,7 +10,14 @@ import { render } from '@testing-library/react';
|
|||
import React from 'react';
|
||||
import { VulnerabilitiesInsight } from './vulnerabilities_insight';
|
||||
import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
|
||||
import { DocumentDetailsContext } from '../context';
|
||||
import { mockFlyoutApi } from '../mocks/mock_flyout_context';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { mockContextValue } from '../mocks/mock_context';
|
||||
import { HostPreviewPanelKey } from '../../../entity_details/host_right';
|
||||
import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview';
|
||||
|
||||
jest.mock('@kbn/expandable-flyout');
|
||||
jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview');
|
||||
|
||||
const hostName = 'test host';
|
||||
|
@ -19,15 +26,21 @@ const testId = 'test';
|
|||
const renderVulnerabilitiesInsight = () => {
|
||||
return render(
|
||||
<TestProviders>
|
||||
<VulnerabilitiesInsight hostName={hostName} data-test-subj={testId} />
|
||||
<DocumentDetailsContext.Provider value={mockContextValue}>
|
||||
<VulnerabilitiesInsight hostName={hostName} data-test-subj={testId} />
|
||||
</DocumentDetailsContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
};
|
||||
|
||||
describe('VulnerabilitiesInsight', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({
|
||||
data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } },
|
||||
data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, NONE: 0 } },
|
||||
});
|
||||
|
||||
const { getByTestId } = renderVulnerabilitiesInsight();
|
||||
|
@ -35,6 +48,24 @@ describe('VulnerabilitiesInsight', () => {
|
|||
expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens host preview when click on count badge', () => {
|
||||
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({
|
||||
data: { count: { CRITICAL: 1, HIGH: 2, MEDIUM: 1, LOW: 2, NONE: 2 } },
|
||||
});
|
||||
const { getByTestId } = renderVulnerabilitiesInsight();
|
||||
expect(getByTestId(`${testId}-count`)).toHaveTextContent('8');
|
||||
|
||||
getByTestId(`${testId}-count`).click();
|
||||
expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({
|
||||
id: HostPreviewPanelKey,
|
||||
params: {
|
||||
hostName,
|
||||
banner: HOST_PREVIEW_BANNER,
|
||||
scopeId: mockContextValue.scopeId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders null when data is not available', () => {
|
||||
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({});
|
||||
|
||||
|
|
|
@ -6,12 +6,16 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui';
|
||||
import { EuiFlexItem, type EuiFlexGroupProps, useEuiTheme } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { css } from '@emotion/css';
|
||||
import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
|
||||
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
|
||||
import { getVulnerabilityStats, hasVulnerabilitiesData } from '@kbn/cloud-security-posture';
|
||||
import { InsightDistributionBar } from './insight_distribution_bar';
|
||||
import { FormattedCount } from '../../../../common/components/formatted_number';
|
||||
import { PreviewLink } from '../../../shared/components/preview_link';
|
||||
import { useDocumentDetailsContext } from '../context';
|
||||
|
||||
interface VulnerabilitiesInsightProps {
|
||||
/**
|
||||
|
@ -29,13 +33,15 @@ interface VulnerabilitiesInsightProps {
|
|||
}
|
||||
|
||||
/*
|
||||
* Displays a distribution bar with the count of critical vulnerabilities for a given host
|
||||
* Displays a distribution bar and the total vulnerabilities count for a given host
|
||||
*/
|
||||
export const VulnerabilitiesInsight: React.FC<VulnerabilitiesInsightProps> = ({
|
||||
hostName,
|
||||
direction,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}) => {
|
||||
const { scopeId, isPreview } = useDocumentDetailsContext();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { data } = useVulnerabilitiesPreview({
|
||||
query: buildEntityFlyoutPreviewQuery('host.name', hostName),
|
||||
sort: [],
|
||||
|
@ -44,6 +50,11 @@ export const VulnerabilitiesInsight: React.FC<VulnerabilitiesInsightProps> = ({
|
|||
});
|
||||
|
||||
const { CRITICAL = 0, HIGH = 0, MEDIUM = 0, LOW = 0, NONE = 0 } = data?.count || {};
|
||||
const totalVulnerabilities = useMemo(
|
||||
() => CRITICAL + HIGH + MEDIUM + LOW + NONE,
|
||||
[CRITICAL, HIGH, MEDIUM, LOW, NONE]
|
||||
);
|
||||
|
||||
const hasVulnerabilitiesFindings = useMemo(
|
||||
() =>
|
||||
hasVulnerabilitiesData({
|
||||
|
@ -68,6 +79,28 @@ export const VulnerabilitiesInsight: React.FC<VulnerabilitiesInsightProps> = ({
|
|||
[CRITICAL, HIGH, MEDIUM, LOW, NONE]
|
||||
);
|
||||
|
||||
const count = useMemo(
|
||||
() => (
|
||||
<div
|
||||
css={css`
|
||||
margin-top: ${euiTheme.size.xs};
|
||||
margin-bottom: ${euiTheme.size.xs};
|
||||
`}
|
||||
>
|
||||
<PreviewLink
|
||||
field={'host.name'}
|
||||
value={hostName}
|
||||
scopeId={scopeId}
|
||||
isPreview={isPreview}
|
||||
data-test-subj={`${dataTestSubj}-count`}
|
||||
>
|
||||
<FormattedCount count={totalVulnerabilities} />
|
||||
</PreviewLink>
|
||||
</div>
|
||||
),
|
||||
[totalVulnerabilities, hostName, scopeId, isPreview, dataTestSubj, euiTheme.size]
|
||||
);
|
||||
|
||||
if (!hasVulnerabilitiesFindings) return null;
|
||||
|
||||
return (
|
||||
|
@ -80,7 +113,7 @@ export const VulnerabilitiesInsight: React.FC<VulnerabilitiesInsightProps> = ({
|
|||
/>
|
||||
}
|
||||
stats={vulnerabilitiesStats}
|
||||
count={CRITICAL}
|
||||
count={count}
|
||||
direction={direction}
|
||||
data-test-subj={`${dataTestSubj}-distribution-bar`}
|
||||
/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue