Skip to content

Commit

Permalink
[Cloud Security] Alerts Preview for Host Name (elastic#197102)
Browse files Browse the repository at this point in the history
## Summary
<img width="1447" alt="Screenshot 2024-10-21 at 10 38 49 PM"
src="https://github.com/user-attachments/assets/e7d011dd-8245-4bf8-ad96-a4fd634e82c1">

This PR is for Alerts preview component in Contextual Flyout (Host Name)

(cherry picked from commit 675b54b)
  • Loading branch information
animehart committed Nov 13, 2024
1 parent 1b38ec1 commit 011d3d8
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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 { AlertsPreview } from './alerts_preview';
import { TestProviders } from '../../../common/mock/test_providers';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import type { ParsedAlertsData } from '../../../overview/components/detection_response/alerts_by_status/types';

const mockAlertsData: ParsedAlertsData = {
open: {
total: 3,
severities: [
{ key: 'low', value: 2, label: 'Low' },
{ key: 'medium', value: 1, label: 'Medium' },
],
},
acknowledged: {
total: 2,
severities: [
{ key: 'low', value: 1, label: 'Low' },
{ key: 'high', value: 1, label: 'High' },
],
},
};

jest.mock(
'../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
);
jest.mock('@kbn/expandable-flyout');

describe('AlertsPreview', () => {
const mockOpenLeftPanel = jest.fn();

beforeEach(() => {
(useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel });
});
afterEach(() => {
jest.clearAllMocks();
});

it('renders', () => {
const { getByTestId } = render(
<TestProviders>
<AlertsPreview alertsData={mockAlertsData} />
</TestProviders>
);

expect(getByTestId('securitySolutionFlyoutInsightsAlertsTitleText')).toBeInTheDocument();
});

it('renders correct alerts number', () => {
const { getByTestId } = render(
<TestProviders>
<AlertsPreview alertsData={mockAlertsData} />
</TestProviders>
);

expect(getByTestId('securitySolutionFlyoutInsightsAlertsCount').textContent).toEqual('5');
});

it('should render the correct number of distribution bar section based on the number of severities', () => {
const { queryAllByTestId } = render(
<TestProviders>
<AlertsPreview alertsData={mockAlertsData} />
</TestProviders>
);

expect(queryAllByTestId('AlertsPreviewDistributionBarTestId__part').length).toEqual(3);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* 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 { capitalize } from 'lodash';
import type { EuiThemeComputed } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle, useEuiTheme } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DistributionBar } from '@kbn/security-solution-distribution-bar';
import { getAbbreviatedNumber } from '@kbn/cloud-security-posture-common';
import { ExpandablePanel } from '../../../flyout/shared/components/expandable_panel';
import { getSeverityColor } from '../../../detections/components/alerts_kpis/severity_level_panel/helpers';
import type {
AlertsByStatus,
ParsedAlertsData,
} from '../../../overview/components/detection_response/alerts_by_status/types';

const AlertsCount = ({
alertsTotal,
euiTheme,
}: {
alertsTotal: number;
euiTheme: EuiThemeComputed<{}>;
}) => {
return (
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>
<EuiTitle size="s">
<h1 data-test-subj={'securitySolutionFlyoutInsightsAlertsCount'}>
{getAbbreviatedNumber(alertsTotal)}
</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiText
size="m"
css={{
fontWeight: euiTheme.font.weight.semiBold,
}}
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.insights.alerts.alertsCountDescription"
defaultMessage="Alerts"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
);
};

export const AlertsPreview = ({
alertsData,
isPreviewMode,
}: {
alertsData: ParsedAlertsData;
isPreviewMode?: boolean;
}) => {
const { euiTheme } = useEuiTheme();

const severityMap = new Map<string, number>();

(Object.keys(alertsData || {}) as AlertsByStatus[]).forEach((status) => {
if (alertsData?.[status]?.severities) {
alertsData?.[status]?.severities.forEach((severity) => {
const currentSeverity = severityMap.get(severity.key) || 0;
severityMap.set(severity.key, currentSeverity + severity.value);
});
}
});

const alertStats = Array.from(severityMap, ([key, count]) => ({
key: capitalize(key),
count,
color: getSeverityColor(key),
}));

const totalAlertsCount = alertStats.reduce((total, item) => total + item.count, 0);

return (
<ExpandablePanel
header={{
title: (
<EuiText
size="xs"
css={{
fontWeight: euiTheme.font.weight.semiBold,
}}
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.insights.alerts.alertsTitle"
defaultMessage="Alerts"
/>
</EuiText>
),
}}
data-test-subj={'securitySolutionFlyoutInsightsAlerts'}
>
<EuiFlexGroup gutterSize="none">
<AlertsCount alertsTotal={totalAlertsCount} euiTheme={euiTheme} />
<EuiFlexItem grow={2}>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem />
<EuiFlexItem>
<EuiSpacer />
<DistributionBar
stats={alertStats.reverse()}
data-test-subj="AlertsPreviewDistributionBarTestId"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</ExpandablePanel>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,22 @@

import { EuiAccordion, EuiHorizontalRule, EuiSpacer, EuiTitle, useEuiTheme } from '@elastic/eui';

import React from 'react';
import React, { useMemo } from 'react';
import { css } from '@emotion/react';
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 { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
import { hasVulnerabilitiesData } from '@kbn/cloud-security-posture';
import { FILTER_CLOSED } from '../../../common/types';
import { MisconfigurationsPreview } from './misconfiguration/misconfiguration_preview';
import { VulnerabilitiesPreview } from './vulnerabilities/vulnerabilities_preview';
import { AlertsPreview } from './alerts/alerts_preview';
import { useGlobalTime } from '../../common/containers/use_global_time';
import type { ParsedAlertsData } from '../../overview/components/detection_response/alerts_by_status/types';
import { DETECTION_RESPONSE_ALERTS_BY_STATUS_ID } from '../../overview/components/detection_response/alerts_by_status/types';
import { useAlertsByStatus } from '../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index';

export const EntityInsight = <T,>({
name,
Expand Down Expand Up @@ -60,6 +67,39 @@ export const EntityInsight = <T,>({

const isVulnerabilitiesFindingForHost = hasVulnerabilitiesFindings && fieldName === 'host.name';

const { signalIndexName } = useSignalIndex();

const entityFilter = useMemo(() => ({ field: fieldName, value: name }), [fieldName, name]);

const { to, from } = useGlobalTime();

const { items: alertsData } = useAlertsByStatus({
entityFilter,
signalIndexName,
queryId: DETECTION_RESPONSE_ALERTS_BY_STATUS_ID,
to,
from,
});

const filteredAlertsData: ParsedAlertsData = alertsData
? Object.fromEntries(Object.entries(alertsData).filter(([key]) => key !== FILTER_CLOSED))
: {};

const alertsOpenCount = filteredAlertsData?.open?.total || 0;

const alertsAcknowledgedCount = filteredAlertsData?.acknowledged?.total || 0;

const alertsCount = alertsOpenCount + alertsAcknowledgedCount;

if (alertsCount > 0) {
insightContent.push(
<>
<AlertsPreview alertsData={filteredAlertsData} isPreviewMode={isPreviewMode} />
<EuiSpacer size="s" />
</>
);
}

if (hasMisconfigurationFindings)
insightContent.push(
<>
Expand All @@ -76,7 +116,8 @@ export const EntityInsight = <T,>({
);
return (
<>
{(hasMisconfigurationFindings ||
{(insightContent.length > 0 ||
hasMisconfigurationFindings ||
(isVulnerabilitiesFindingForHost && hasVulnerabilitiesFindings)) && (
<>
<EuiAccordion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ export const VulnerabilitiesPreview = ({
callback: goToEntityInsightTab,
tooltip: (
<FormattedMessage
id="xpack.securitySolution.flyout.right.insights.misconfiguration.misconfigurationTooltip"
defaultMessage="Show all misconfiguration findings"
id="xpack.securitySolution.flyout.right.insights.vulnerabilities.vulnerabilitiesTooltip"
defaultMessage="Show all vulnerabilities findings"
/>
),
}
Expand Down

0 comments on commit 011d3d8

Please sign in to comment.