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

[Security Solution] Add alert and cloud insights to document flyout #195509

Merged
merged 6 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
@@ -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();
});
});
christineweng marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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 '../../../flyout/document_details/shared/components/insight_distribution_bar';
import { getFindingsStats } from './misconfiguration_preview';

/*
* Misconfigurations insight in the alert/event flyout.
*/
christineweng marked this conversation as resolved.
Show resolved Hide resolved
export const MisconfigurationsInsight = ({
name,
fieldName,
direction,
'data-test-subj': dataTestSubj,
}: {
name: string;
fieldName: 'host.name' | 'user.name';
direction?: EuiFlexGroupProps['direction'];
['data-test-subj']?: string;
christineweng marked this conversation as resolved.
Show resolved Hide resolved
}) => {
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';
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
@@ -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();
});
});
christineweng marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -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 { 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 '../../../flyout/document_details/shared/components/insight_distribution_bar';

/*
* Vulnerabilities insight in the alert/event flyout.
*/
christineweng marked this conversation as resolved.
Show resolved Hide resolved
export const VulnerabilitiesInsight = ({
hostName,
direction,
'data-test-subj': dataTestSubj,
}: {
hostName: string;
direction?: EuiFlexGroupProps['direction'];
['data-test-subj']?: string;
christineweng marked this conversation as resolved.
Show resolved Hide resolved
}) => {
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