Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -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}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
});
});
Loading