[8.x] [Security Solution] Add alert and cloud insights to document flyout (#195509) (#195825)

# 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:
Kibana Machine 2024-10-11 09:30:49 +11:00 committed by GitHub
parent 2ac46f4d0a
commit e435c47a8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 946 additions and 7 deletions

View file

@ -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>

View file

@ -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
});

View file

@ -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 (

View file

@ -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 [
{

View file

@ -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();
});
});
});

View file

@ -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}>

View file

@ -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 =

View file

@ -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();
});
});
});

View file

@ -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}>

View file

@ -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();
});
});
});

View file

@ -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>
);
};

View file

@ -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 */

View file

@ -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();
});
});
});

View file

@ -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>
);
};

View file

@ -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();
});
});

View file

@ -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';

View file

@ -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();
});
});

View file

@ -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';

View file

@ -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();
});
});

View file

@ -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';

View file

@ -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;

View file

@ -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();
});
});

View file

@ -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';