Skip to content

Commit

Permalink
[Security Solution] Add alert and cloud insights to document flyout (e…
Browse files Browse the repository at this point in the history
…lastic#195509)

## Summary

This PR adds alert count, misconfiguration and vulnerabilities insights
to alert/event flyout. If data is not available, the insights are
hidden.


[Mocks](https://www.figma.com/design/ubvhBGHee58diJNvSiy0GZ/%5B8.%2B%5D-%5BAlerts%5D-Expandable-Event-Flyout?node-id=8017-179782&node-type=canvas&t=0YjHfPi9zOUFUScc-0)


![image](https://github.com/user-attachments/assets/ba706ab8-448a-4286-8229-c4c398136638)

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
  • Loading branch information
christineweng authored Oct 10, 2024
1 parent 3974845 commit cd217c0
Show file tree
Hide file tree
Showing 23 changed files with 946 additions and 7 deletions.
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

0 comments on commit cd217c0

Please sign in to comment.