mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution] Add alert and cloud insights to document flyout (#195509)](https://github.com/elastic/kibana/pull/195509) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"christineweng","email":"18648970+christineweng@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-10-10T20:46:51Z","message":"[Security Solution] Add alert and cloud insights to document flyout (#195509)\n\n## Summary\r\n\r\nThis PR adds alert count, misconfiguration and vulnerabilities insights\r\nto alert/event flyout. If data is not available, the insights are\r\nhidden.\r\n\r\n\r\n[Mocks](ba706ab8
-448a-4286-8229-c4c398136638)\r\n\r\n### Checklist\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios","sha":"cd217c072fc786cb76ee47d885501688507c2dde","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["v9.0.0","Team:Threat Hunting","release_note:feature","Team:Threat Hunting:Investigations","backport:prev-major","8.16 candidate","v8.16.0"],"title":"[Security Solution] Add alert and cloud insights to document flyout","number":195509,"url":"https://github.com/elastic/kibana/pull/195509","mergeCommit":{"message":"[Security Solution] Add alert and cloud insights to document flyout (#195509)\n\n## Summary\r\n\r\nThis PR adds alert count, misconfiguration and vulnerabilities insights\r\nto alert/event flyout. If data is not available, the insights are\r\nhidden.\r\n\r\n\r\n[Mocks](ba706ab8
-448a-4286-8229-c4c398136638)\r\n\r\n### Checklist\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios","sha":"cd217c072fc786cb76ee47d885501688507c2dde"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/195509","number":195509,"mergeCommit":{"message":"[Security Solution] Add alert and cloud insights to document flyout (#195509)\n\n## Summary\r\n\r\nThis PR adds alert count, misconfiguration and vulnerabilities insights\r\nto alert/event flyout. If data is not available, the insights are\r\nhidden.\r\n\r\n\r\n[Mocks](ba706ab8
-448a-4286-8229-c4c398136638)\r\n\r\n### Checklist\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios","sha":"cd217c072fc786cb76ee47d885501688507c2dde"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: christineweng <18648970+christineweng@users.noreply.github.com>
This commit is contained in:
parent
2ac46f4d0a
commit
e435c47a8a
23 changed files with 946 additions and 7 deletions
|
@ -70,6 +70,14 @@ export const DistributionBar = () => {
|
|||
<DistributionBarComponent stats={mockStatsAlerts} />
|
||||
<EuiSpacer size={'m'} />
|
||||
</React.Fragment>,
|
||||
<React.Fragment key={'hideLastTooltip'}>
|
||||
<EuiTitle size={'xs'}>
|
||||
<h4>{'Hide last tooltip'}</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size={'s'} />
|
||||
<DistributionBarComponent stats={mockStatsAlerts} hideLastTooltip />
|
||||
<EuiSpacer size={'m'} />
|
||||
</React.Fragment>,
|
||||
<React.Fragment key={'empty'}>
|
||||
<EuiTitle size={'xs'}>
|
||||
<h4>{'Empty state'}</h4>
|
||||
|
|
|
@ -79,5 +79,67 @@ describe('DistributionBar', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should render last tooltip by default', () => {
|
||||
const stats = [
|
||||
{
|
||||
key: 'low',
|
||||
count: 9,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
key: 'medium',
|
||||
count: 90,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
key: 'high',
|
||||
count: 900,
|
||||
color: 'red',
|
||||
},
|
||||
];
|
||||
|
||||
const { container } = render(
|
||||
<DistributionBar stats={stats} data-test-subj={testSubj} hideLastTooltip={true} />
|
||||
);
|
||||
expect(container).toBeInTheDocument();
|
||||
const parts = container.querySelectorAll(`[classname*="distribution_bar--tooltip"]`);
|
||||
parts.forEach((part, index) => {
|
||||
if (index < parts.length - 1) {
|
||||
expect(part).toHaveStyle({ opacity: 0 });
|
||||
} else {
|
||||
expect(part).toHaveStyle({ opacity: 1 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render last tooltip when hideLastTooltip is true', () => {
|
||||
const stats = [
|
||||
{
|
||||
key: 'low',
|
||||
count: 9,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
key: 'medium',
|
||||
count: 90,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
key: 'high',
|
||||
count: 900,
|
||||
color: 'red',
|
||||
},
|
||||
];
|
||||
|
||||
const { container } = render(
|
||||
<DistributionBar stats={stats} data-test-subj={testSubj} hideLastTooltip={true} />
|
||||
);
|
||||
expect(container).toBeInTheDocument();
|
||||
const parts = container.querySelectorAll(`[classname*="distribution_bar--tooltip"]`);
|
||||
parts.forEach((part) => {
|
||||
expect(part).toHaveStyle({ opacity: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
// todo: test tooltip visibility logic
|
||||
});
|
||||
|
|
|
@ -13,6 +13,8 @@ import { css } from '@emotion/react';
|
|||
export interface DistributionBarProps {
|
||||
/** distribution data points */
|
||||
stats: Array<{ key: string; count: number; color: string; label?: React.ReactNode }>;
|
||||
/** hide the label above the bar at first render */
|
||||
hideLastTooltip?: boolean;
|
||||
/** data-test-subj used for querying the component in tests */
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
@ -136,18 +138,21 @@ export const DistributionBar: React.FC<DistributionBarProps> = React.memo(functi
|
|||
props
|
||||
) {
|
||||
const styles = useStyles();
|
||||
const { stats, 'data-test-subj': dataTestSubj } = props;
|
||||
const { stats, 'data-test-subj': dataTestSubj, hideLastTooltip } = props;
|
||||
const parts = stats.map((stat) => {
|
||||
const partStyle = [
|
||||
styles.part.base,
|
||||
styles.part.tick,
|
||||
styles.part.hover,
|
||||
styles.part.lastTooltip,
|
||||
css`
|
||||
background-color: ${stat.color};
|
||||
flex: ${stat.count};
|
||||
`,
|
||||
];
|
||||
if (!hideLastTooltip) {
|
||||
partStyle.push(styles.part.lastTooltip);
|
||||
}
|
||||
|
||||
const prettyNumber = numeral(stat.count).format('0,0a');
|
||||
|
||||
return (
|
||||
|
|
|
@ -35,7 +35,7 @@ const FIRST_RECORD_PAGINATION = {
|
|||
querySize: 1,
|
||||
};
|
||||
|
||||
const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => {
|
||||
export const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => {
|
||||
if (passedFindingsStats === 0 && failedFindingsStats === 0) return [];
|
||||
return [
|
||||
{
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
|
||||
import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
|
||||
import type { Anomalies } from '../../../../common/components/ml/types';
|
||||
import { DocumentDetailsContext } from '../../shared/context';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
|
@ -24,6 +26,9 @@ import {
|
|||
HOST_DETAILS_LINK_TEST_ID,
|
||||
HOST_DETAILS_RELATED_USERS_LINK_TEST_ID,
|
||||
HOST_DETAILS_RELATED_USERS_IP_LINK_TEST_ID,
|
||||
HOST_DETAILS_MISCONFIGURATIONS_TEST_ID,
|
||||
HOST_DETAILS_VULNERABILITIES_TEST_ID,
|
||||
HOST_DETAILS_ALERT_COUNT_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '@kbn/security-solution-common';
|
||||
import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score';
|
||||
|
@ -35,8 +40,11 @@ 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';
|
||||
import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details';
|
||||
import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
|
||||
|
||||
jest.mock('@kbn/expandable-flyout');
|
||||
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
|
||||
jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview');
|
||||
|
||||
jest.mock('../../../../common/hooks/use_experimental_features');
|
||||
const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||
|
@ -104,6 +112,10 @@ const mockUseHostsRelatedUsers = useHostRelatedUsers as jest.Mock;
|
|||
jest.mock('../../../../entity_analytics/api/hooks/use_risk_score');
|
||||
const mockUseRiskScore = useRiskScore as jest.Mock;
|
||||
|
||||
jest.mock(
|
||||
'../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
|
||||
);
|
||||
|
||||
const timestamp = '2022-07-25T08:20:18.966Z';
|
||||
|
||||
const defaultProps = {
|
||||
|
@ -158,6 +170,9 @@ describe('<HostDetails />', () => {
|
|||
mockUseRiskScore.mockReturnValue(mockRiskScoreResponse);
|
||||
mockUseHostsRelatedUsers.mockReturnValue(mockRelatedUsersResponse);
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
|
||||
(useMisconfigurationPreview as jest.Mock).mockReturnValue({});
|
||||
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({});
|
||||
(useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
|
||||
});
|
||||
|
||||
it('should render host details correctly', () => {
|
||||
|
@ -296,4 +311,41 @@ describe('<HostDetails />', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('distribution bar insights', () => {
|
||||
it('should not render if no data is available', () => {
|
||||
const { queryByTestId } = renderHostDetails(mockContextValue);
|
||||
expect(queryByTestId(HOST_DETAILS_MISCONFIGURATIONS_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(HOST_DETAILS_VULNERABILITIES_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(HOST_DETAILS_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render alert count when data is available', () => {
|
||||
(useSummaryChartData as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
items: [{ key: 'high', value: 78, label: 'High' }],
|
||||
});
|
||||
|
||||
const { getByTestId } = renderHostDetails(mockContextValue);
|
||||
expect(getByTestId(HOST_DETAILS_ALERT_COUNT_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render misconfiguration when data is available', () => {
|
||||
(useMisconfigurationPreview as jest.Mock).mockReturnValue({
|
||||
data: { count: { passed: 1, failed: 2 } },
|
||||
});
|
||||
|
||||
const { getByTestId } = renderHostDetails(mockContextValue);
|
||||
expect(getByTestId(HOST_DETAILS_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render vulnerabilities when data is available', () => {
|
||||
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({
|
||||
data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } },
|
||||
});
|
||||
|
||||
const { getByTestId } = renderHostDetails(mockContextValue);
|
||||
expect(getByTestId(HOST_DETAILS_VULNERABILITIES_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,6 +18,8 @@ import {
|
|||
EuiToolTip,
|
||||
EuiIcon,
|
||||
EuiPanel,
|
||||
EuiHorizontalRule,
|
||||
EuiFlexGrid,
|
||||
} from '@elastic/eui';
|
||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
@ -51,6 +53,9 @@ import {
|
|||
HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID,
|
||||
HOST_DETAILS_RELATED_USERS_LINK_TEST_ID,
|
||||
HOST_DETAILS_RELATED_USERS_IP_LINK_TEST_ID,
|
||||
HOST_DETAILS_ALERT_COUNT_TEST_ID,
|
||||
HOST_DETAILS_MISCONFIGURATIONS_TEST_ID,
|
||||
HOST_DETAILS_VULNERABILITIES_TEST_ID,
|
||||
} from './test_ids';
|
||||
import {
|
||||
USER_NAME_FIELD_NAME,
|
||||
|
@ -63,6 +68,9 @@ import { PreviewLink } from '../../../shared/components/preview_link';
|
|||
import { HostPreviewPanelKey } from '../../../entity_details/host_right';
|
||||
import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview';
|
||||
import type { NarrowDateRange } from '../../../../common/components/ml/types';
|
||||
import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight';
|
||||
import { VulnerabilitiesInsight } from '../../shared/components/vulnerabilities_insight';
|
||||
import { AlertCountInsight } from '../../shared/components/alert_count_insight';
|
||||
|
||||
const HOST_DETAILS_ID = 'entities-hosts-details';
|
||||
const RELATED_USERS_ID = 'entities-hosts-related-users';
|
||||
|
@ -337,6 +345,28 @@ export const HostDetails: React.FC<HostDetailsProps> = ({ hostName, timestamp, s
|
|||
)}
|
||||
</AnomalyTableProvider>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiFlexGrid responsive={false} columns={3} gutterSize="xl">
|
||||
<AlertCountInsight
|
||||
fieldName={'host.name'}
|
||||
name={hostName}
|
||||
direction="column"
|
||||
data-test-subj={HOST_DETAILS_ALERT_COUNT_TEST_ID}
|
||||
/>
|
||||
<MisconfigurationsInsight
|
||||
fieldName={'host.name'}
|
||||
name={hostName}
|
||||
direction="column"
|
||||
data-test-subj={HOST_DETAILS_MISCONFIGURATIONS_TEST_ID}
|
||||
/>
|
||||
<VulnerabilitiesInsight
|
||||
hostName={hostName}
|
||||
direction="column"
|
||||
data-test-subj={HOST_DETAILS_VULNERABILITIES_TEST_ID}
|
||||
/>
|
||||
</EuiFlexGrid>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiPanel hasBorder={true}>
|
||||
<EuiFlexGroup direction="row" gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -43,6 +43,9 @@ export const PREVALENCE_DETAILS_TABLE_UPSELL_CELL_TEST_ID =
|
|||
export const ENTITIES_DETAILS_TEST_ID = `${PREFIX}EntitiesDetails` as const;
|
||||
export const USER_DETAILS_TEST_ID = `${PREFIX}UsersDetails` as const;
|
||||
export const USER_DETAILS_LINK_TEST_ID = `${USER_DETAILS_TEST_ID}TitleLink` as const;
|
||||
export const USER_DETAILS_ALERT_COUNT_TEST_ID = `${USER_DETAILS_TEST_ID}AlertCount` as const;
|
||||
export const USER_DETAILS_MISCONFIGURATIONS_TEST_ID =
|
||||
`${USER_DETAILS_TEST_ID}Misconfigurations` as const;
|
||||
export const USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID =
|
||||
`${USER_DETAILS_TEST_ID}RelatedHostsTable` as const;
|
||||
export const USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID =
|
||||
|
@ -53,6 +56,11 @@ export const USER_DETAILS_INFO_TEST_ID = 'user-overview' as const;
|
|||
|
||||
export const HOST_DETAILS_TEST_ID = `${PREFIX}HostsDetails` as const;
|
||||
export const HOST_DETAILS_LINK_TEST_ID = `${HOST_DETAILS_TEST_ID}TitleLink` as const;
|
||||
export const HOST_DETAILS_ALERT_COUNT_TEST_ID = `${HOST_DETAILS_TEST_ID}AlertCount` as const;
|
||||
export const HOST_DETAILS_MISCONFIGURATIONS_TEST_ID =
|
||||
`${HOST_DETAILS_TEST_ID}Misconfigurations` as const;
|
||||
export const HOST_DETAILS_VULNERABILITIES_TEST_ID =
|
||||
`${HOST_DETAILS_TEST_ID}Vulnerabilities` as const;
|
||||
export const HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID =
|
||||
`${HOST_DETAILS_TEST_ID}RelatedUsersTable` as const;
|
||||
export const HOST_DETAILS_RELATED_USERS_LINK_TEST_ID =
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
|
||||
import type { Anomalies } from '../../../../common/components/ml/types';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { DocumentDetailsContext } from '../../shared/context';
|
||||
|
@ -24,6 +25,8 @@ import {
|
|||
USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID,
|
||||
USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID,
|
||||
USER_DETAILS_RELATED_HOSTS_IP_LINK_TEST_ID,
|
||||
USER_DETAILS_MISCONFIGURATIONS_TEST_ID,
|
||||
USER_DETAILS_ALERT_COUNT_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '@kbn/security-solution-common';
|
||||
import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score';
|
||||
|
@ -35,8 +38,10 @@ 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';
|
||||
import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details';
|
||||
import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
|
||||
|
||||
jest.mock('@kbn/expandable-flyout');
|
||||
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
|
||||
|
||||
jest.mock('../../../../common/hooks/use_experimental_features');
|
||||
const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||
|
@ -101,6 +106,10 @@ const mockUseUsersRelatedHosts = useUserRelatedHosts as jest.Mock;
|
|||
jest.mock('../../../../entity_analytics/api/hooks/use_risk_score');
|
||||
const mockUseRiskScore = useRiskScore as jest.Mock;
|
||||
|
||||
jest.mock(
|
||||
'../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
|
||||
);
|
||||
|
||||
const timestamp = '2022-07-25T08:20:18.966Z';
|
||||
|
||||
const defaultProps = {
|
||||
|
@ -155,6 +164,8 @@ describe('<UserDetails />', () => {
|
|||
mockUseRiskScore.mockReturnValue(mockRiskScoreResponse);
|
||||
mockUseUsersRelatedHosts.mockReturnValue(mockRelatedHostsResponse);
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
|
||||
(useMisconfigurationPreview as jest.Mock).mockReturnValue({});
|
||||
(useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
|
||||
});
|
||||
|
||||
it('should render user details correctly', () => {
|
||||
|
@ -278,4 +289,31 @@ describe('<UserDetails />', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('distribution bar insights', () => {
|
||||
it('should not render if no data is available', () => {
|
||||
const { queryByTestId } = renderUserDetails(mockContextValue);
|
||||
expect(queryByTestId(USER_DETAILS_MISCONFIGURATIONS_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(USER_DETAILS_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render alert count when data is available', () => {
|
||||
(useSummaryChartData as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
items: [{ key: 'high', value: 78, label: 'High' }],
|
||||
});
|
||||
|
||||
const { getByTestId } = renderUserDetails(mockContextValue);
|
||||
expect(getByTestId(USER_DETAILS_ALERT_COUNT_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render misconfiguration when data is available', () => {
|
||||
(useMisconfigurationPreview as jest.Mock).mockReturnValue({
|
||||
data: { count: { passed: 1, failed: 2 } },
|
||||
});
|
||||
|
||||
const { getByTestId } = renderUserDetails(mockContextValue);
|
||||
expect(getByTestId(USER_DETAILS_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,6 +18,8 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiToolTip,
|
||||
EuiPanel,
|
||||
EuiHorizontalRule,
|
||||
EuiFlexGrid,
|
||||
} from '@elastic/eui';
|
||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
@ -51,6 +53,8 @@ import {
|
|||
USER_DETAILS_TEST_ID,
|
||||
USER_DETAILS_RELATED_HOSTS_LINK_TEST_ID,
|
||||
USER_DETAILS_RELATED_HOSTS_IP_LINK_TEST_ID,
|
||||
USER_DETAILS_MISCONFIGURATIONS_TEST_ID,
|
||||
USER_DETAILS_ALERT_COUNT_TEST_ID,
|
||||
} from './test_ids';
|
||||
import {
|
||||
HOST_NAME_FIELD_NAME,
|
||||
|
@ -63,6 +67,8 @@ import { UserPreviewPanelKey } from '../../../entity_details/user_right';
|
|||
import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview';
|
||||
import { PreviewLink } from '../../../shared/components/preview_link';
|
||||
import type { NarrowDateRange } from '../../../../common/components/ml/types';
|
||||
import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight';
|
||||
import { AlertCountInsight } from '../../shared/components/alert_count_insight';
|
||||
|
||||
const USER_DETAILS_ID = 'entities-users-details';
|
||||
const RELATED_HOSTS_ID = 'entities-users-related-hosts';
|
||||
|
@ -340,6 +346,22 @@ export const UserDetails: React.FC<UserDetailsProps> = ({ userName, timestamp, s
|
|||
)}
|
||||
</AnomalyTableProvider>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiFlexGrid responsive={false} columns={3} gutterSize="xl">
|
||||
<AlertCountInsight
|
||||
fieldName={'user.name'}
|
||||
name={userName}
|
||||
direction="column"
|
||||
data-test-subj={USER_DETAILS_ALERT_COUNT_TEST_ID}
|
||||
/>
|
||||
<MisconfigurationsInsight
|
||||
fieldName={'user.name'}
|
||||
name={userName}
|
||||
direction="column"
|
||||
data-test-subj={USER_DETAILS_MISCONFIGURATIONS_TEST_ID}
|
||||
/>
|
||||
</EuiFlexGrid>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiPanel hasBorder={true}>
|
||||
<EuiFlexGroup direction="row" gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
|
||||
import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { HostEntityOverview, HOST_PREVIEW_BANNER } from './host_entity_overview';
|
||||
import { useHostDetails } from '../../../../explore/hosts/containers/hosts/details';
|
||||
|
@ -16,6 +18,9 @@ import {
|
|||
ENTITIES_HOST_OVERVIEW_LINK_TEST_ID,
|
||||
ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID,
|
||||
ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID,
|
||||
ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID,
|
||||
ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID,
|
||||
ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { DocumentDetailsContext } from '../../shared/context';
|
||||
import { mockContextValue } from '../../shared/mocks/mock_context';
|
||||
|
@ -29,6 +34,7 @@ import { ENTITIES_TAB_ID } from '../../left/components/entities_details';
|
|||
import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score';
|
||||
import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context';
|
||||
import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock';
|
||||
import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
|
||||
|
||||
const hostName = 'host';
|
||||
const osFamily = 'Windows';
|
||||
|
@ -46,6 +52,17 @@ const panelContextValue = {
|
|||
};
|
||||
|
||||
jest.mock('@kbn/expandable-flyout');
|
||||
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
|
||||
jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview');
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const actual = jest.requireActual('react-router-dom');
|
||||
return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
'../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
|
||||
);
|
||||
|
||||
const mockedTelemetry = createTelemetryServiceMock();
|
||||
jest.mock('../../../../common/lib/kibana', () => {
|
||||
|
@ -99,6 +116,9 @@ describe('<HostEntityContent />', () => {
|
|||
beforeAll(() => {
|
||||
jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi);
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
|
||||
(useMisconfigurationPreview as jest.Mock).mockReturnValue({});
|
||||
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({});
|
||||
(useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
|
||||
});
|
||||
|
||||
describe('license is valid', () => {
|
||||
|
@ -150,6 +170,7 @@ describe('<HostEntityContent />', () => {
|
|||
);
|
||||
expect(getByTestId(ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('license is not valid', () => {
|
||||
it('should render os family and last seen', () => {
|
||||
mockUseHostDetails.mockReturnValue([false, { hostDetails: hostData }]);
|
||||
|
@ -210,4 +231,48 @@ describe('<HostEntityContent />', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('distribution bar insights', () => {
|
||||
beforeEach(() => {
|
||||
mockUseHostDetails.mockReturnValue([false, { hostDetails: hostData }]);
|
||||
mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: true });
|
||||
});
|
||||
|
||||
it('should not render if no data is available', () => {
|
||||
const { queryByTestId } = renderHostEntityContent();
|
||||
expect(
|
||||
queryByTestId(ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID)
|
||||
).not.toBeInTheDocument();
|
||||
expect(queryByTestId(ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render alert count when data is available', () => {
|
||||
(useSummaryChartData as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
items: [{ key: 'high', value: 78, label: 'High' }],
|
||||
});
|
||||
|
||||
const { getByTestId } = renderHostEntityContent();
|
||||
expect(getByTestId(ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render misconfiguration when data is available', () => {
|
||||
(useMisconfigurationPreview as jest.Mock).mockReturnValue({
|
||||
data: { count: { passed: 1, failed: 2 } },
|
||||
});
|
||||
|
||||
const { getByTestId } = renderHostEntityContent();
|
||||
expect(getByTestId(ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render vulnerabilities when data is available', () => {
|
||||
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({
|
||||
data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } },
|
||||
});
|
||||
|
||||
const { getByTestId } = renderHostEntityContent();
|
||||
expect(getByTestId(ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -52,11 +52,17 @@ import {
|
|||
ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID,
|
||||
ENTITIES_HOST_OVERVIEW_LINK_TEST_ID,
|
||||
ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID,
|
||||
ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID,
|
||||
ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID,
|
||||
ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys';
|
||||
import { LeftPanelInsightsTab } from '../../left';
|
||||
import { RiskScoreDocTooltip } from '../../../../overview/components/common';
|
||||
import { PreviewLink } from '../../../shared/components/preview_link';
|
||||
import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight';
|
||||
import { VulnerabilitiesInsight } from '../../shared/components/vulnerabilities_insight';
|
||||
import { AlertCountInsight } from '../../shared/components/alert_count_insight';
|
||||
|
||||
const HOST_ICON = 'storage';
|
||||
|
||||
|
@ -196,12 +202,12 @@ export const HostEntityOverview: React.FC<HostEntityOverviewProps> = ({ hostName
|
|||
return (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
gutterSize="m"
|
||||
responsive={false}
|
||||
data-test-subj={ENTITIES_HOST_OVERVIEW_TEST_ID}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="m" responsive={false}>
|
||||
<EuiFlexGroup gutterSize="s" responsive={false} alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={HOST_ICON} />
|
||||
</EuiFlexItem>
|
||||
|
@ -270,6 +276,20 @@ export const HostEntityOverview: React.FC<HostEntityOverviewProps> = ({ hostName
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<AlertCountInsight
|
||||
fieldName={'host.name'}
|
||||
name={hostName}
|
||||
data-test-subj={ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID}
|
||||
/>
|
||||
<MisconfigurationsInsight
|
||||
fieldName={'host.name'}
|
||||
name={hostName}
|
||||
data-test-subj={ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID}
|
||||
/>
|
||||
<VulnerabilitiesInsight
|
||||
hostName={hostName}
|
||||
data-test-subj={ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -121,6 +121,10 @@ export const ENTITIES_USER_OVERVIEW_LAST_SEEN_TEST_ID =
|
|||
`${ENTITIES_USER_OVERVIEW_TEST_ID}LastSeen` as const;
|
||||
export const ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID =
|
||||
`${ENTITIES_USER_OVERVIEW_TEST_ID}RiskLevel` as const;
|
||||
export const ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID =
|
||||
`${ENTITIES_USER_OVERVIEW_TEST_ID}AlertCount` as const;
|
||||
export const ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID =
|
||||
`${ENTITIES_USER_OVERVIEW_TEST_ID}Misconfigurations` as const;
|
||||
|
||||
export const ENTITIES_HOST_OVERVIEW_TEST_ID = `${INSIGHTS_ENTITIES_TEST_ID}HostOverview` as const;
|
||||
export const ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID =
|
||||
|
@ -132,6 +136,12 @@ export const ENTITIES_HOST_OVERVIEW_LAST_SEEN_TEST_ID =
|
|||
`${ENTITIES_HOST_OVERVIEW_TEST_ID}LastSeen` as const;
|
||||
export const ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID =
|
||||
`${ENTITIES_HOST_OVERVIEW_TEST_ID}RiskLevel` as const;
|
||||
export const ENTITIES_HOST_OVERVIEW_ALERT_COUNT_TEST_ID =
|
||||
`${ENTITIES_HOST_OVERVIEW_TEST_ID}AlertCount` as const;
|
||||
export const ENTITIES_HOST_OVERVIEW_MISCONFIGURATIONS_TEST_ID =
|
||||
`${ENTITIES_HOST_OVERVIEW_TEST_ID}Misconfigurations` as const;
|
||||
export const ENTITIES_HOST_OVERVIEW_VULNERABILITIES_TEST_ID =
|
||||
`${ENTITIES_HOST_OVERVIEW_TEST_ID}Vulnerabilities` as const;
|
||||
|
||||
/* Threat intelligence */
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
|
||||
import { UserEntityOverview, USER_PREVIEW_BANNER } from './user_entity_overview';
|
||||
import { useFirstLastSeen } from '../../../../common/containers/use_first_last_seen';
|
||||
import {
|
||||
|
@ -15,6 +16,8 @@ import {
|
|||
ENTITIES_USER_OVERVIEW_LINK_TEST_ID,
|
||||
ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID,
|
||||
ENTITIES_USER_OVERVIEW_LOADING_TEST_ID,
|
||||
ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID,
|
||||
ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { useObservedUserDetails } from '../../../../explore/users/containers/users/observed_details';
|
||||
import { mockContextValue } from '../../shared/mocks/mock_context';
|
||||
|
@ -28,6 +31,7 @@ import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
|||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context';
|
||||
import { UserPreviewPanelKey } from '../../../entity_details/user_right';
|
||||
import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
|
||||
|
||||
const userName = 'user';
|
||||
const domain = 'n54bg2lfc7';
|
||||
|
@ -45,6 +49,18 @@ const panelContextValue = {
|
|||
};
|
||||
|
||||
jest.mock('@kbn/expandable-flyout');
|
||||
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const actual = jest.requireActual('react-router-dom');
|
||||
return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
'../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
|
||||
);
|
||||
|
||||
jest.mock('../../../../common/hooks/use_experimental_features');
|
||||
const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||
|
@ -85,6 +101,8 @@ describe('<UserEntityOverview />', () => {
|
|||
beforeAll(() => {
|
||||
jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi);
|
||||
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
|
||||
(useMisconfigurationPreview as jest.Mock).mockReturnValue({});
|
||||
(useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
|
||||
});
|
||||
|
||||
describe('license is valid', () => {
|
||||
|
@ -211,4 +229,38 @@ describe('<UserEntityOverview />', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('distribution bar insights', () => {
|
||||
beforeEach(() => {
|
||||
mockUseUserDetails.mockReturnValue([false, { userDetails: userData }]);
|
||||
mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: true });
|
||||
});
|
||||
|
||||
it('should not render if no data is available', () => {
|
||||
const { queryByTestId } = renderUserEntityOverview();
|
||||
expect(
|
||||
queryByTestId(ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID)
|
||||
).not.toBeInTheDocument();
|
||||
expect(queryByTestId(ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render alert count when data is available', () => {
|
||||
(useSummaryChartData as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
items: [{ key: 'high', value: 78, label: 'High' }],
|
||||
});
|
||||
|
||||
const { getByTestId } = renderUserEntityOverview();
|
||||
expect(getByTestId(ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render misconfiguration when data is available', () => {
|
||||
(useMisconfigurationPreview as jest.Mock).mockReturnValue({
|
||||
data: { count: { passed: 1, failed: 2 } },
|
||||
});
|
||||
|
||||
const { getByTestId } = renderUserEntityOverview();
|
||||
expect(getByTestId(ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -53,10 +53,14 @@ import {
|
|||
ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID,
|
||||
ENTITIES_USER_OVERVIEW_LINK_TEST_ID,
|
||||
ENTITIES_USER_OVERVIEW_LOADING_TEST_ID,
|
||||
ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID,
|
||||
ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { useObservedUserDetails } from '../../../../explore/users/containers/users/observed_details';
|
||||
import { RiskScoreDocTooltip } from '../../../../overview/components/common';
|
||||
import { PreviewLink } from '../../../shared/components/preview_link';
|
||||
import { MisconfigurationsInsight } from '../../shared/components/misconfiguration_insight';
|
||||
import { AlertCountInsight } from '../../shared/components/alert_count_insight';
|
||||
|
||||
const USER_ICON = 'user';
|
||||
|
||||
|
@ -196,12 +200,12 @@ export const UserEntityOverview: React.FC<UserEntityOverviewProps> = ({ userName
|
|||
return (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
gutterSize="m"
|
||||
responsive={false}
|
||||
data-test-subj={ENTITIES_USER_OVERVIEW_TEST_ID}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="m" responsive={false}>
|
||||
<EuiFlexGroup gutterSize="s" responsive={false} alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={USER_ICON} />
|
||||
</EuiFlexItem>
|
||||
|
@ -270,6 +274,16 @@ export const UserEntityOverview: React.FC<UserEntityOverviewProps> = ({ userName
|
|||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<AlertCountInsight
|
||||
fieldName={'user.name'}
|
||||
name={userName}
|
||||
data-test-subj={ENTITIES_USER_OVERVIEW_ALERT_COUNT_TEST_ID}
|
||||
/>
|
||||
<MisconfigurationsInsight
|
||||
fieldName={'user.name'}
|
||||
name={userName}
|
||||
data-test-subj={ENTITIES_USER_OVERVIEW_MISCONFIGURATIONS_TEST_ID}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { AlertCountInsight } from './alert_count_insight';
|
||||
import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const actual = jest.requireActual('react-router-dom');
|
||||
return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
|
||||
});
|
||||
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
|
||||
jest.mock(
|
||||
'../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
|
||||
);
|
||||
|
||||
const fieldName = 'host.name';
|
||||
const name = 'test host';
|
||||
const testId = 'test';
|
||||
|
||||
const renderAlertCountInsight = () => {
|
||||
return render(
|
||||
<TestProviders>
|
||||
<AlertCountInsight name={name} fieldName={fieldName} data-test-subj={testId} />
|
||||
</TestProviders>
|
||||
);
|
||||
};
|
||||
|
||||
describe('AlertCountInsight', () => {
|
||||
it('renders', () => {
|
||||
(useSummaryChartData as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
items: [
|
||||
{ key: 'high', value: 78, label: 'High' },
|
||||
{ key: 'low', value: 46, label: 'Low' },
|
||||
{ key: 'medium', value: 32, label: 'Medium' },
|
||||
{ key: 'critical', value: 21, label: 'Critical' },
|
||||
],
|
||||
});
|
||||
const { getByTestId } = renderAlertCountInsight();
|
||||
expect(getByTestId(testId)).toBeInTheDocument();
|
||||
expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders loading spinner if data is being fetched', () => {
|
||||
(useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: true, items: [] });
|
||||
const { getByTestId } = renderAlertCountInsight();
|
||||
expect(getByTestId(`${testId}-loading-spinner`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders null if no misconfiguration data found', () => {
|
||||
(useSummaryChartData as jest.Mock).mockReturnValue({ isLoading: false, items: [] });
|
||||
const { container } = renderAlertCountInsight();
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 { v4 as uuid } from 'uuid';
|
||||
import { EuiLoadingSpinner, EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { InsightDistributionBar } from './insight_distribution_bar';
|
||||
import { severityAggregations } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/aggregations';
|
||||
import { useSummaryChartData } from '../../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data';
|
||||
import {
|
||||
getIsAlertsBySeverityData,
|
||||
getSeverityColor,
|
||||
} from '../../../../detections/components/alerts_kpis/severity_level_panel/helpers';
|
||||
|
||||
const ENTITY_ALERT_COUNT_ID = 'entity-alert-count';
|
||||
|
||||
interface AlertCountInsightProps {
|
||||
/**
|
||||
* The name of the entity to filter the alerts by.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The field name to filter the alerts by.
|
||||
*/
|
||||
fieldName: 'host.name' | 'user.name';
|
||||
/**
|
||||
* The direction of the flex group.
|
||||
*/
|
||||
direction?: EuiFlexGroupProps['direction'];
|
||||
/**
|
||||
* The data-test-subj to use for the component.
|
||||
*/
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Displays a distribution bar with the count of critical alerts for a given entity
|
||||
*/
|
||||
export const AlertCountInsight: React.FC<AlertCountInsightProps> = ({
|
||||
name,
|
||||
fieldName,
|
||||
direction,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}) => {
|
||||
const uniqueQueryId = useMemo(() => `${ENTITY_ALERT_COUNT_ID}-${uuid()}`, []);
|
||||
const entityFilter = useMemo(() => ({ field: fieldName, value: name }), [fieldName, name]);
|
||||
|
||||
const { items, isLoading } = useSummaryChartData({
|
||||
aggregations: severityAggregations,
|
||||
entityFilter,
|
||||
uniqueQueryId,
|
||||
signalIndexName: null,
|
||||
});
|
||||
|
||||
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,
|
||||
[data]
|
||||
);
|
||||
|
||||
if (!isLoading && items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<EuiFlexItem data-test-subj={dataTestSubj}>
|
||||
{isLoading ? (
|
||||
<EuiLoadingSpinner size="m" data-test-subj={`${dataTestSubj}-loading-spinner`} />
|
||||
) : (
|
||||
<InsightDistributionBar
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.insights.alertCountTitle"
|
||||
defaultMessage="Alerts:"
|
||||
/>
|
||||
}
|
||||
stats={alertStats}
|
||||
count={count}
|
||||
direction={direction}
|
||||
data-test-subj={`${dataTestSubj}-distribution-bar`}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
AlertCountInsight.displayName = 'AlertCountInsight';
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { InsightDistributionBar } from './insight_distribution_bar';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
|
||||
const title = 'test title';
|
||||
const count = 10;
|
||||
const testId = 'test-id';
|
||||
const stats = [
|
||||
{
|
||||
key: 'passed',
|
||||
count: 90,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
key: 'failed',
|
||||
count: 10,
|
||||
color: 'red',
|
||||
},
|
||||
];
|
||||
|
||||
describe('<InsightDistributionBar />', () => {
|
||||
it('should render', () => {
|
||||
const { getByTestId, getByText } = render(
|
||||
<TestProviders>
|
||||
<InsightDistributionBar title={title} stats={stats} count={count} data-test-subj={testId} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(getByTestId(testId)).toBeInTheDocument();
|
||||
expect(getByText(title)).toBeInTheDocument();
|
||||
expect(getByTestId(`${testId}-badge`)).toHaveTextContent(`${count}`);
|
||||
expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/css';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiText,
|
||||
EuiBadge,
|
||||
useEuiTheme,
|
||||
useEuiFontSize,
|
||||
type EuiFlexGroupProps,
|
||||
} from '@elastic/eui';
|
||||
import { DistributionBar } from '@kbn/security-solution-distribution-bar';
|
||||
import { FormattedCount } from '../../../../common/components/formatted_number';
|
||||
|
||||
export interface InsightDistributionBarProps {
|
||||
/**
|
||||
* Title of the insight
|
||||
*/
|
||||
title: string | React.ReactNode;
|
||||
/**
|
||||
* Distribution stats to display
|
||||
*/
|
||||
stats: Array<{ key: string; count: number; color: string; label?: React.ReactNode }>;
|
||||
/**
|
||||
* Count to be displayed in the badge
|
||||
*/
|
||||
count: number;
|
||||
/**
|
||||
* Flex direction of the component
|
||||
*/
|
||||
direction?: EuiFlexGroupProps['direction'];
|
||||
/**
|
||||
* Optional test id
|
||||
*/
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
// Displays a distribution bar with a count badge
|
||||
export const InsightDistributionBar: React.FC<InsightDistributionBarProps> = ({
|
||||
title,
|
||||
stats,
|
||||
count,
|
||||
direction = 'row',
|
||||
'data-test-subj': dataTestSubj,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const xsFontSize = useEuiFontSize('xs').fontSize;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction={direction} data-test-subj={dataTestSubj} responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiText
|
||||
css={css`
|
||||
font-size: ${xsFontSize};
|
||||
font-weight: ${euiTheme.font.weight.bold};
|
||||
`}
|
||||
>
|
||||
{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>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
InsightDistributionBar.displayName = 'InsightDistributionBar';
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { 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';
|
||||
|
||||
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
|
||||
|
||||
const fieldName = 'host.name';
|
||||
const name = 'test host';
|
||||
const testId = 'test';
|
||||
|
||||
const renderMisconfigurationsInsight = () => {
|
||||
return render(
|
||||
<TestProviders>
|
||||
<MisconfigurationsInsight name={name} fieldName={fieldName} data-test-subj={testId} />
|
||||
</TestProviders>
|
||||
);
|
||||
};
|
||||
|
||||
describe('MisconfigurationsInsight', () => {
|
||||
it('renders', () => {
|
||||
(useMisconfigurationPreview as jest.Mock).mockReturnValue({
|
||||
data: { count: { passed: 1, failed: 2 } },
|
||||
});
|
||||
const { getByTestId } = renderMisconfigurationsInsight();
|
||||
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();
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
|
@ -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, { useMemo } from 'react';
|
||||
import { EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
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';
|
||||
|
||||
interface MisconfigurationsInsightProps {
|
||||
/**
|
||||
* Entity name to retrieve misconfigurations for
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Indicator whether the entity is host or user
|
||||
*/
|
||||
fieldName: 'host.name' | 'user.name';
|
||||
/**
|
||||
* The direction of the flex group
|
||||
*/
|
||||
direction?: EuiFlexGroupProps['direction'];
|
||||
/**
|
||||
* The data-test-subj to use for the component
|
||||
*/
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Displays a distribution bar with the count of failed misconfigurations for a given entity
|
||||
*/
|
||||
export const MisconfigurationsInsight: React.FC<MisconfigurationsInsightProps> = ({
|
||||
name,
|
||||
fieldName,
|
||||
direction,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}) => {
|
||||
const { data } = useMisconfigurationPreview({
|
||||
query: buildEntityFlyoutPreviewQuery(fieldName, name),
|
||||
sort: [],
|
||||
enabled: true,
|
||||
pageSize: 1,
|
||||
});
|
||||
|
||||
const passedFindings = data?.count.passed || 0;
|
||||
const failedFindings = data?.count.failed || 0;
|
||||
const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0;
|
||||
|
||||
const misconfigurationsStats = useMemo(
|
||||
() => getFindingsStats(passedFindings, failedFindings),
|
||||
[passedFindings, failedFindings]
|
||||
);
|
||||
|
||||
if (!hasMisconfigurationFindings) return null;
|
||||
|
||||
return (
|
||||
<EuiFlexItem data-test-subj={dataTestSubj}>
|
||||
<InsightDistributionBar
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.insights.misconfigurationsTitle"
|
||||
defaultMessage="Misconfigurations:"
|
||||
/>
|
||||
}
|
||||
stats={misconfigurationsStats}
|
||||
count={failedFindings}
|
||||
direction={direction}
|
||||
data-test-subj={`${dataTestSubj}-distribution-bar`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
MisconfigurationsInsight.displayName = 'MisconfigurationsInsight';
|
|
@ -12,3 +12,6 @@ export const FLYOUT_PREVIEW_LINK_TEST_ID = `${PREFIX}PreviewLink` as const;
|
|||
|
||||
export const SESSION_VIEW_UPSELL_TEST_ID = `${PREFIX}SessionViewUpsell` as const;
|
||||
export const SESSION_VIEW_NO_DATA_TEST_ID = `${PREFIX}SessionViewNoData` as const;
|
||||
|
||||
export const MISCONFIGURATIONS_TEST_ID = `${PREFIX}Misconfigurations` as const;
|
||||
export const VULNERABILITIES_TEST_ID = `${PREFIX}Vulnerabilities` as const;
|
||||
|
|
|
@ -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 { TestProviders } from '../../../../common/mock';
|
||||
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';
|
||||
|
||||
jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview');
|
||||
|
||||
const hostName = 'test host';
|
||||
const testId = 'test';
|
||||
|
||||
const renderVulnerabilitiesInsight = () => {
|
||||
return render(
|
||||
<TestProviders>
|
||||
<VulnerabilitiesInsight hostName={hostName} data-test-subj={testId} />
|
||||
</TestProviders>
|
||||
);
|
||||
};
|
||||
|
||||
describe('VulnerabilitiesInsight', () => {
|
||||
it('renders', () => {
|
||||
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({
|
||||
data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } },
|
||||
});
|
||||
|
||||
const { getByTestId } = renderVulnerabilitiesInsight();
|
||||
expect(getByTestId(testId)).toBeInTheDocument();
|
||||
expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders null when data is not available', () => {
|
||||
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({});
|
||||
|
||||
const { container } = renderVulnerabilitiesInsight();
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 { EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
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';
|
||||
|
||||
interface VulnerabilitiesInsightProps {
|
||||
/**
|
||||
* Host name to retrieve vulnerabilities for
|
||||
*/
|
||||
hostName: string;
|
||||
/**
|
||||
* The direction of the flex group
|
||||
*/
|
||||
direction?: EuiFlexGroupProps['direction'];
|
||||
/**
|
||||
* The data-test-subj to use for the component
|
||||
*/
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Displays a distribution bar with the count of critical vulnerabilities for a given host
|
||||
*/
|
||||
export const VulnerabilitiesInsight: React.FC<VulnerabilitiesInsightProps> = ({
|
||||
hostName,
|
||||
direction,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}) => {
|
||||
const { data } = useVulnerabilitiesPreview({
|
||||
query: buildEntityFlyoutPreviewQuery('host.name', hostName),
|
||||
sort: [],
|
||||
enabled: true,
|
||||
pageSize: 1,
|
||||
});
|
||||
|
||||
const { CRITICAL = 0, HIGH = 0, MEDIUM = 0, LOW = 0, NONE = 0 } = data?.count || {};
|
||||
const hasVulnerabilitiesFindings = useMemo(
|
||||
() =>
|
||||
hasVulnerabilitiesData({
|
||||
critical: CRITICAL,
|
||||
high: HIGH,
|
||||
medium: MEDIUM,
|
||||
low: LOW,
|
||||
none: NONE,
|
||||
}),
|
||||
[CRITICAL, HIGH, MEDIUM, LOW, NONE]
|
||||
);
|
||||
|
||||
const vulnerabilitiesStats = useMemo(
|
||||
() =>
|
||||
getVulnerabilityStats({
|
||||
critical: CRITICAL,
|
||||
high: HIGH,
|
||||
medium: MEDIUM,
|
||||
low: LOW,
|
||||
none: NONE,
|
||||
}),
|
||||
[CRITICAL, HIGH, MEDIUM, LOW, NONE]
|
||||
);
|
||||
|
||||
if (!hasVulnerabilitiesFindings) return null;
|
||||
|
||||
return (
|
||||
<EuiFlexItem data-test-subj={dataTestSubj}>
|
||||
<InsightDistributionBar
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.insights.vulnerabilitiesTitle"
|
||||
defaultMessage="Vulnerabilities:"
|
||||
/>
|
||||
}
|
||||
stats={vulnerabilitiesStats}
|
||||
count={CRITICAL}
|
||||
direction={direction}
|
||||
data-test-subj={`${dataTestSubj}-distribution-bar`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
VulnerabilitiesInsight.displayName = 'VulnerabilitiesInsight';
|
Loading…
Add table
Add a link
Reference in a new issue