[Cloud Security] Alerts Datagrids for Contextual Flyout (#199573)

## Summary

This PR is for Alerts Datagrid component in Contextual Flyout

This PR is for Alerts Datagrid in Contextual Flyout for User name and
Host name
<img width="1480" alt="Screenshot 2024-11-14 at 9 08 26 AM"
src="https://github.com/user-attachments/assets/46a254c8-b7f1-4b63-9637-2b1c281d5502">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Rickyanto Ang 2024-11-14 11:07:26 -08:00 committed by GitHub
parent 16127fcc8f
commit ab965f75a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 693 additions and 30 deletions

View file

@ -10,6 +10,7 @@ import {
defaultErrorMessage,
buildMutedRulesFilter,
buildEntityFlyoutPreviewQuery,
buildEntityAlertsQuery,
} from './helpers';
const fallbackMessage = 'thisIsAFallBackMessage';
@ -182,4 +183,78 @@ describe('test helper methods', () => {
expect(buildEntityFlyoutPreviewQuery(field)).toEqual(expectedQuery);
});
});
describe('buildEntityAlertsQuery', () => {
const getExpectedAlertsQuery = (size?: number) => {
return {
size: size || 0,
_source: false,
fields: [
'_id',
'_index',
'kibana.alert.rule.uuid',
'kibana.alert.severity',
'kibana.alert.rule.name',
'kibana.alert.workflow_status',
],
query: {
bool: {
filter: [
{
bool: {
must: [],
filter: [
{
match_phrase: {
'host.name': {
query: 'exampleHost',
},
},
},
],
should: [],
must_not: [],
},
},
{
range: {
'@timestamp': {
gte: 'Today',
lte: 'Tomorrow',
},
},
},
{
terms: {
'kibana.alert.workflow_status': ['open', 'acknowledged'],
},
},
],
},
},
};
};
it('should return the correct query when given all params', () => {
const field = 'host.name';
const query = 'exampleHost';
const to = 'Tomorrow';
const from = 'Today';
const size = 100;
expect(buildEntityAlertsQuery(field, to, from, query, size)).toEqual(
getExpectedAlertsQuery(size)
);
});
it('should return the correct query when not given size', () => {
const field = 'host.name';
const query = 'exampleHost';
const to = 'Tomorrow';
const from = 'Today';
const size = undefined;
expect(buildEntityAlertsQuery(field, to, from, query)).toEqual(getExpectedAlertsQuery(size));
});
});
});

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types';
import { i18n } from '@kbn/i18n';
import type { CspBenchmarkRulesStates } from '../schema/rules/latest';
@ -62,3 +63,59 @@ export const buildEntityFlyoutPreviewQuery = (field: string, queryValue?: string
},
};
};
export const buildEntityAlertsQuery = (
field: string,
to: string,
from: string,
queryValue?: string,
size?: number
) => {
return {
size: size || 0,
_source: false,
fields: [
'_id',
'_index',
'kibana.alert.rule.uuid',
'kibana.alert.severity',
'kibana.alert.rule.name',
'kibana.alert.workflow_status',
],
query: {
bool: {
filter: [
{
bool: {
must: [],
filter: [
{
match_phrase: {
[field]: {
query: queryValue,
},
},
},
],
should: [],
must_not: [],
},
},
{
range: {
'@timestamp': {
gte: from,
lte: to,
},
},
},
{
terms: {
'kibana.alert.workflow_status': ['open', 'acknowledged'],
},
},
],
},
},
};
};

View file

@ -40,10 +40,11 @@ export const useMisconfigurationFindings = (options: UseCspOptions) => {
params: buildMisconfigurationsFindingsQuery(options, rulesStates!),
})
);
if (!aggregations) throw new Error('expected aggregations to be defined');
if (!aggregations && options.ignore_unavailable === false)
throw new Error('expected aggregations to be defined');
return {
count: getMisconfigurationAggregationCount(aggregations.count.buckets),
count: getMisconfigurationAggregationCount(aggregations?.count.buckets),
rows: hits.hits.map((finding) => ({
result: finding._source?.result,
rule: finding?._source?.rule,

View file

@ -11,6 +11,9 @@ 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';
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 { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score';
const mockAlertsData: ParsedAlertsData = {
open: {
@ -29,9 +32,10 @@ const mockAlertsData: ParsedAlertsData = {
},
};
jest.mock(
'../../../detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data'
);
// Mock hooks
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview');
jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview');
jest.mock('../../../entity_analytics/api/hooks/use_risk_score');
jest.mock('@kbn/expandable-flyout');
describe('AlertsPreview', () => {
@ -39,6 +43,13 @@ describe('AlertsPreview', () => {
beforeEach(() => {
(useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel });
(useVulnerabilitiesPreview as jest.Mock).mockReturnValue({
data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } },
});
(useRiskScore as jest.Mock).mockReturnValue({ data: [{ host: { risk: 75 } }] });
(useMisconfigurationPreview as jest.Mock).mockReturnValue({
data: { count: { passed: 1, failed: 1 } },
});
});
afterEach(() => {
jest.clearAllMocks();
@ -47,17 +58,17 @@ describe('AlertsPreview', () => {
it('renders', () => {
const { getByTestId } = render(
<TestProviders>
<AlertsPreview alertsData={mockAlertsData} />
<AlertsPreview alertsData={mockAlertsData} name="host1" fieldName="host.name" />
</TestProviders>
);
expect(getByTestId('securitySolutionFlyoutInsightsAlertsTitleText')).toBeInTheDocument();
expect(getByTestId('securitySolutionFlyoutInsightsAlertsTitleLink')).toBeInTheDocument();
});
it('renders correct alerts number', () => {
const { getByTestId } = render(
<TestProviders>
<AlertsPreview alertsData={mockAlertsData} />
<AlertsPreview alertsData={mockAlertsData} name="host1" fieldName="host.name" />
</TestProviders>
);
@ -67,7 +78,7 @@ describe('AlertsPreview', () => {
it('should render the correct number of distribution bar section based on the number of severities', () => {
const { queryAllByTestId } = render(
<TestProviders>
<AlertsPreview alertsData={mockAlertsData} />
<AlertsPreview alertsData={mockAlertsData} name="host1" fieldName="host.name" />
</TestProviders>
);

View file

@ -5,19 +5,40 @@
* 2.0.
*/
import React from 'react';
import React, { useCallback, useMemo } 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 {
buildEntityFlyoutPreviewQuery,
getAbbreviatedNumber,
} from '@kbn/cloud-security-posture-common';
import { hasVulnerabilitiesData } from '@kbn/cloud-security-posture';
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 { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import type {
AlertsByStatus,
ParsedAlertsData,
} from '../../../overview/components/detection_response/alerts_by_status/types';
import { ExpandablePanel } from '../../../flyout/shared/components/expandable_panel';
import { getSeverityColor } from '../../../detections/components/alerts_kpis/severity_level_panel/helpers';
import type { HostRiskScore, UserRiskScore } from '../../../../common/search_strategy';
import {
buildHostNamesFilter,
buildUserNamesFilter,
RiskScoreEntity,
} from '../../../../common/search_strategy';
import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score';
import { FIRST_RECORD_PAGINATION } from '../../../entity_analytics/common';
import { HostDetailsPanelKey } from '../../../flyout/entity_details/host_details_left';
import {
EntityDetailsLeftPanelTab,
CspInsightLeftPanelSubTab,
} from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header';
import { UserDetailsPanelKey } from '../../../flyout/entity_details/user_details_left';
const AlertsCount = ({
alertsTotal,
@ -56,9 +77,13 @@ const AlertsCount = ({
export const AlertsPreview = ({
alertsData,
fieldName,
name,
isPreviewMode,
}: {
alertsData: ParsedAlertsData;
fieldName: string;
name: string;
isPreviewMode?: boolean;
}) => {
const { euiTheme } = useEuiTheme();
@ -82,9 +107,120 @@ export const AlertsPreview = ({
const totalAlertsCount = alertStats.reduce((total, item) => total + item.count, 0);
const { data } = useMisconfigurationPreview({
query: buildEntityFlyoutPreviewQuery(fieldName, name),
sort: [],
enabled: true,
pageSize: 1,
ignore_unavailable: true,
});
const isUsingHostName = fieldName === 'host.name';
const passedFindings = data?.count.passed || 0;
const failedFindings = data?.count.failed || 0;
const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0;
const { data: vulnerabilitiesData } = useVulnerabilitiesPreview({
query: buildEntityFlyoutPreviewQuery('host.name', name),
sort: [],
enabled: true,
pageSize: 1,
});
const {
CRITICAL = 0,
HIGH = 0,
MEDIUM = 0,
LOW = 0,
NONE = 0,
} = vulnerabilitiesData?.count || {};
const hasVulnerabilitiesFindings = hasVulnerabilitiesData({
critical: CRITICAL,
high: HIGH,
medium: MEDIUM,
low: LOW,
none: NONE,
});
const buildFilterQuery = useMemo(
() => (isUsingHostName ? buildHostNamesFilter([name]) : buildUserNamesFilter([name])),
[isUsingHostName, name]
);
const riskScoreState = useRiskScore({
riskEntity: isUsingHostName ? RiskScoreEntity.host : RiskScoreEntity.user,
filterQuery: buildFilterQuery,
onlyLatest: false,
pagination: FIRST_RECORD_PAGINATION,
});
const { data: hostRisk } = riskScoreState;
const riskData = hostRisk?.[0];
const isRiskScoreExist = isUsingHostName
? !!(riskData as HostRiskScore)?.host.risk
: !!(riskData as UserRiskScore)?.user.risk;
const hasNonClosedAlerts = totalAlertsCount > 0;
const { openLeftPanel } = useExpandableFlyoutApi();
const goToEntityInsightTab = useCallback(() => {
openLeftPanel({
id: isUsingHostName ? HostDetailsPanelKey : UserDetailsPanelKey,
params: isUsingHostName
? {
name,
isRiskScoreExist,
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
hasNonClosedAlerts,
path: {
tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS,
subTab: CspInsightLeftPanelSubTab.ALERTS,
},
}
: {
user: { name },
isRiskScoreExist,
hasMisconfigurationFindings,
hasNonClosedAlerts,
path: {
tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS,
subTab: CspInsightLeftPanelSubTab.ALERTS,
},
},
});
}, [
hasMisconfigurationFindings,
hasNonClosedAlerts,
hasVulnerabilitiesFindings,
isRiskScoreExist,
isUsingHostName,
name,
openLeftPanel,
]);
const link = useMemo(
() =>
!isPreviewMode
? {
callback: goToEntityInsightTab,
tooltip: (
<FormattedMessage
id="xpack.securitySolution.flyout.right.insights.alerts.alertsTooltip"
defaultMessage="Show all alerts"
/>
),
}
: undefined,
[isPreviewMode, goToEntityInsightTab]
);
return (
<ExpandablePanel
header={{
iconType: !isPreviewMode && hasNonClosedAlerts ? 'arrowStart' : '',
title: (
<EuiText
size="xs"
@ -98,6 +234,7 @@ export const AlertsPreview = ({
/>
</EuiText>
),
link: totalAlertsCount > 0 ? link : undefined,
}}
data-test-subj={'securitySolutionFlyoutInsightsAlerts'}
>

View file

@ -0,0 +1,265 @@
/*
* 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, { memo, useCallback, useEffect, useState } from 'react';
import { capitalize } from 'lodash';
import type { Criteria, EuiBasicTableColumn } from '@elastic/eui';
import { EuiSpacer, EuiPanel, EuiText, EuiBasicTable, EuiIcon, EuiLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DistributionBar } from '@kbn/security-solution-distribution-bar';
import {
ENTITY_FLYOUT_EXPAND_MISCONFIGURATION_VIEW_VISITS,
uiMetricService,
} from '@kbn/cloud-security-posture-common/utils/ui_metrics';
import { METRIC_TYPE } from '@kbn/analytics';
import { buildEntityAlertsQuery } from '@kbn/cloud-security-posture-common/utils/helpers';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { TableId } from '@kbn/securitysolution-data-table';
import {
OPEN_IN_ALERTS_TITLE_HOSTNAME,
OPEN_IN_ALERTS_TITLE_STATUS,
OPEN_IN_ALERTS_TITLE_USERNAME,
} from '../../../overview/components/detection_response/translations';
import { useNavigateToAlertsPageWithFilters } from '../../../common/hooks/use_navigate_to_alerts_page_with_filters';
import { DocumentDetailsPreviewPanelKey } from '../../../flyout/document_details/shared/constants/panel_keys';
import { useGlobalTime } from '../../../common/containers/use_global_time';
import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query';
import { ALERTS_QUERY_NAMES } from '../../../detections/containers/detection_engine/alerts/constants';
import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index';
import { getSeverityColor } from '../../../detections/components/alerts_kpis/severity_level_panel/helpers';
import { SeverityBadge } from '../../../common/components/severity_badge';
import { ALERT_PREVIEW_BANNER } from '../../../flyout/document_details/preview/constants';
import { FILTER_OPEN, FILTER_ACKNOWLEDGED } from '../../../../common/types';
type AlertSeverity = 'low' | 'medium' | 'high' | 'critical';
interface ResultAlertsField {
_id: string[];
_index: string[];
'kibana.alert.rule.uuid': string[];
'kibana.alert.severity': AlertSeverity[];
'kibana.alert.rule.name': string[];
'kibana.alert.workflow_status': string[];
}
interface ContextualFlyoutAlertsField {
id: string;
index: string;
ruleUuid: string;
ruleName: string;
severity: AlertSeverity;
status: string;
}
interface AlertsDetailsFields {
fields: ResultAlertsField;
}
export const AlertsDetailsTable = memo(
({ fieldName, queryName }: { fieldName: 'host.name' | 'user.name'; queryName: string }) => {
useEffect(() => {
uiMetricService.trackUiMetric(
METRIC_TYPE.COUNT,
ENTITY_FLYOUT_EXPAND_MISCONFIGURATION_VIEW_VISITS
);
}, []);
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const alertsPagination = (alerts: ContextualFlyoutAlertsField[]) => {
let pageOfItems;
if (!pageIndex && !pageSize) {
pageOfItems = alerts;
} else {
const startIndex = pageIndex * pageSize;
pageOfItems = alerts?.slice(startIndex, Math.min(startIndex + pageSize, alerts?.length));
}
return {
pageOfItems,
totalItemCount: alerts?.length,
};
};
const { to, from } = useGlobalTime();
const { signalIndexName } = useSignalIndex();
const { data } = useQueryAlerts({
query: buildEntityAlertsQuery(fieldName, to, from, queryName, 500),
queryName: ALERTS_QUERY_NAMES.BY_RULE_BY_STATUS,
indexName: signalIndexName,
});
const alertDataResults = (data?.hits?.hits as AlertsDetailsFields[])?.map(
(item: AlertsDetailsFields) => {
return {
id: item.fields?._id?.[0],
index: item.fields?._index?.[0],
ruleName: item.fields?.['kibana.alert.rule.name']?.[0],
ruleUuid: item.fields?.['kibana.alert.rule.uuid']?.[0],
severity: item.fields?.['kibana.alert.severity']?.[0],
status: item.fields?.['kibana.alert.workflow_status']?.[0],
};
}
);
const severitiesMap = alertDataResults?.map((item) => item.severity) || [];
const alertStats = Object.entries(
severitiesMap.reduce((acc: Record<string, number>, item) => {
acc[item] = (acc[item] || 0) + 1;
return acc;
}, {})
).map(([key, count]) => ({
key: capitalize(key),
count,
color: getSeverityColor(key),
}));
const { pageOfItems, totalItemCount } = alertsPagination(alertDataResults || []);
const pagination = {
pageIndex,
pageSize,
totalItemCount,
pageSizeOptions: [10, 25, 100],
};
const onTableChange = ({ page }: Criteria<ContextualFlyoutAlertsField>) => {
if (page) {
const { index, size } = page;
setPageIndex(index);
setPageSize(size);
}
};
const { openPreviewPanel } = useExpandableFlyoutApi();
const handleOnEventAlertDetailPanelOpened = useCallback(
(eventId: string, indexName: string, tableId: string) => {
openPreviewPanel({
id: DocumentDetailsPreviewPanelKey,
params: {
id: eventId,
indexName,
scopeId: tableId,
isPreviewMode: true,
banner: ALERT_PREVIEW_BANNER,
},
});
},
[openPreviewPanel]
);
const tableId = TableId.alertsOnRuleDetailsPage;
const columns: Array<EuiBasicTableColumn<ContextualFlyoutAlertsField>> = [
{
field: 'id',
name: '',
width: '5%',
render: (id: string, alert: ContextualFlyoutAlertsField) => (
<EuiLink onClick={() => handleOnEventAlertDetailPanelOpened(id, alert.index, tableId)}>
<EuiIcon type={'expand'} />
</EuiLink>
),
},
{
field: 'ruleName',
render: (ruleName: string) => <EuiText size="s">{ruleName}</EuiText>,
name: i18n.translate(
'xpack.securitySolution.flyout.left.insights.alerts.table.ruleNameColumnName',
{
defaultMessage: 'Rule',
}
),
width: '55%',
},
{
field: 'severity',
render: (severity: AlertSeverity) => (
<EuiText size="s">
<SeverityBadge value={severity} data-test-subj="severityPropertyValue" />
</EuiText>
),
name: i18n.translate(
'xpack.securitySolution.flyout.left.insights.alerts.table.severityColumnName',
{
defaultMessage: 'Severity',
}
),
width: '20%',
},
{
field: 'status',
render: (status: string) => <EuiText size="s">{capitalize(status)}</EuiText>,
name: i18n.translate(
'xpack.securitySolution.flyout.left.insights.alerts.table.statusColumnName',
{
defaultMessage: 'Status',
}
),
width: '20%',
},
];
const openAlertsPageWithFilters = useNavigateToAlertsPageWithFilters();
const openAlertsInAlertsPage = useCallback(
() =>
openAlertsPageWithFilters(
[
{
title:
fieldName === 'host.name'
? OPEN_IN_ALERTS_TITLE_HOSTNAME
: OPEN_IN_ALERTS_TITLE_USERNAME,
selectedOptions: [queryName],
fieldName,
},
{
title: OPEN_IN_ALERTS_TITLE_STATUS,
selectedOptions: [FILTER_OPEN, FILTER_ACKNOWLEDGED],
fieldName: 'kibana.alert.workflow_status',
},
],
true
),
[fieldName, openAlertsPageWithFilters, queryName]
);
return (
<>
<EuiPanel hasShadow={false}>
<EuiLink onClick={() => openAlertsInAlertsPage()}>
<h1 data-test-subj={'securitySolutionFlyoutInsightsAlertsCount'}>
{i18n.translate('xpack.securitySolution.flyout.left.insights.alerts.tableTitle', {
defaultMessage: 'Alerts ',
})}
<EuiIcon type={'popout'} />
</h1>
</EuiLink>
<EuiSpacer size="xl" />
<DistributionBar stats={alertStats.reverse()} />
<EuiSpacer size="l" />
<EuiBasicTable
items={pageOfItems || []}
rowHeader="result"
columns={columns}
pagination={pagination}
onChange={onTableChange}
data-test-subj={'securitySolutionFlyoutMisconfigurationFindingsTable'}
/>
</EuiPanel>
</>
);
}
);
AlertsDetailsTable.displayName = 'AlertsDetailsTable';

View file

@ -12,10 +12,10 @@ import { FormattedMessage } from '@kbn/i18n-react';
import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout';
import { useExpandableFlyoutState } from '@kbn/expandable-flyout';
import { i18n } from '@kbn/i18n';
// import type { FlyoutPanels } from '@kbn/expandable-flyout/src/store/state';
import { CspInsightLeftPanelSubTab } from '../../../flyout/entity_details/shared/components/left_panel/left_panel_header';
import { MisconfigurationFindingsDetailsTable } from './misconfiguration_findings_details_table';
import { VulnerabilitiesFindingsDetailsTable } from './vulnerabilities_findings_details_table';
import { AlertsDetailsTable } from './alerts_findings_details_table';
/**
* Insights view displayed in the document details expandable flyout left section
@ -26,6 +26,7 @@ interface CspFlyoutPanelProps extends FlyoutPanelProps {
path: PanelPath;
hasMisconfigurationFindings: boolean;
hasVulnerabilitiesFindings: boolean;
hasNonClosedAlerts: boolean;
};
}
@ -35,7 +36,8 @@ function isCspFlyoutPanelProps(
): panelLeft is CspFlyoutPanelProps {
return (
!!panelLeft?.params?.hasMisconfigurationFindings ||
!!panelLeft?.params?.hasVulnerabilitiesFindings
!!panelLeft?.params?.hasVulnerabilitiesFindings ||
!!panelLeft?.params?.hasNonClosedAlerts
);
}
@ -45,12 +47,14 @@ export const InsightsTabCsp = memo(
let hasMisconfigurationFindings = false;
let hasVulnerabilitiesFindings = false;
let hasNonClosedAlerts = false;
let subTab: string | undefined;
// Check if panels.left is of type CspFlyoutPanelProps and extract values
if (isCspFlyoutPanelProps(panels.left)) {
hasMisconfigurationFindings = panels.left.params.hasMisconfigurationFindings;
hasVulnerabilitiesFindings = panels.left.params.hasVulnerabilitiesFindings;
hasNonClosedAlerts = panels.left.params.hasNonClosedAlerts;
subTab = panels.left.params.path?.subTab;
}
@ -63,6 +67,8 @@ export const InsightsTabCsp = memo(
? CspInsightLeftPanelSubTab.MISCONFIGURATIONS
: hasVulnerabilitiesFindings
? CspInsightLeftPanelSubTab.VULNERABILITIES
: hasNonClosedAlerts
? CspInsightLeftPanelSubTab.ALERTS
: '';
};
@ -71,6 +77,19 @@ export const InsightsTabCsp = memo(
const insightsButtons: EuiButtonGroupOptionProps[] = useMemo(() => {
const buttons: EuiButtonGroupOptionProps[] = [];
if (panels.left?.params?.hasNonClosedAlerts) {
buttons.push({
id: CspInsightLeftPanelSubTab.ALERTS,
label: (
<FormattedMessage
id="xpack.securitySolution.flyout.left.insights.alertsButtonLabel"
defaultMessage="Alerts"
/>
),
'data-test-subj': 'alertsTabDataTestId',
});
}
if (panels.left?.params?.hasMisconfigurationFindings) {
buttons.push({
id: CspInsightLeftPanelSubTab.MISCONFIGURATIONS,
@ -96,9 +115,11 @@ export const InsightsTabCsp = memo(
'data-test-subj': 'vulnerabilitiesTabDataTestId',
});
}
return buttons;
}, [
panels.left?.params?.hasMisconfigurationFindings,
panels.left?.params?.hasNonClosedAlerts,
panels.left?.params?.hasVulnerabilitiesFindings,
]);
@ -130,8 +151,10 @@ export const InsightsTabCsp = memo(
<EuiSpacer size="xl" />
{activeInsightsId === CspInsightLeftPanelSubTab.MISCONFIGURATIONS ? (
<MisconfigurationFindingsDetailsTable fieldName={fieldName} queryName={name} />
) : (
) : activeInsightsId === CspInsightLeftPanelSubTab.VULNERABILITIES ? (
<VulnerabilitiesFindingsDetailsTable queryName={name} />
) : (
<AlertsDetailsTable fieldName={fieldName} queryName={name} />
)}
</>
);

View file

@ -94,7 +94,12 @@ export const EntityInsight = <T,>({
if (alertsCount > 0) {
insightContent.push(
<>
<AlertsPreview alertsData={filteredAlertsData} isPreviewMode={isPreviewMode} />
<AlertsPreview
alertsData={filteredAlertsData}
fieldName={fieldName}
name={name}
isPreviewMode={isPreviewMode}
/>
<EuiSpacer size="s" />
</>
);
@ -103,14 +108,23 @@ export const EntityInsight = <T,>({
if (hasMisconfigurationFindings)
insightContent.push(
<>
<MisconfigurationsPreview name={name} fieldName={fieldName} isPreviewMode={isPreviewMode} />
<MisconfigurationsPreview
name={name}
fieldName={fieldName}
hasNonClosedAlerts={alertsCount > 0}
isPreviewMode={isPreviewMode}
/>
<EuiSpacer size="s" />
</>
);
if (isVulnerabilitiesFindingForHost && hasVulnerabilitiesFindings)
insightContent.push(
<>
<VulnerabilitiesPreview name={name} isPreviewMode={isPreviewMode} />
<VulnerabilitiesPreview
name={name}
isPreviewMode={isPreviewMode}
hasNonClosedAlerts={alertsCount > 0}
/>
<EuiSpacer size="s" />
</>
);

View file

@ -103,10 +103,12 @@ const MisconfigurationPreviewScore = ({
export const MisconfigurationsPreview = ({
name,
fieldName,
hasNonClosedAlerts = false,
isPreviewMode,
}: {
name: string;
fieldName: 'host.name' | 'user.name';
hasNonClosedAlerts?: boolean;
isPreviewMode?: boolean;
}) => {
const { data } = useMisconfigurationPreview({
@ -180,6 +182,7 @@ export const MisconfigurationsPreview = ({
isRiskScoreExist,
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
hasNonClosedAlerts,
path: {
tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS,
subTab: CspInsightLeftPanelSubTab.MISCONFIGURATIONS,
@ -189,11 +192,16 @@ export const MisconfigurationsPreview = ({
user: { name },
isRiskScoreExist,
hasMisconfigurationFindings,
path: { tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS },
hasNonClosedAlerts,
path: {
tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS,
subTab: CspInsightLeftPanelSubTab.MISCONFIGURATIONS,
},
},
});
}, [
hasMisconfigurationFindings,
hasNonClosedAlerts,
hasVulnerabilitiesFindings,
isRiskScoreExist,
isUsingHostName,

View file

@ -72,9 +72,11 @@ const VulnerabilitiesCount = ({
export const VulnerabilitiesPreview = ({
name,
isPreviewMode,
hasNonClosedAlerts = false,
}: {
name: string;
isPreviewMode?: boolean;
hasNonClosedAlerts?: boolean;
}) => {
useEffect(() => {
uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, ENTITY_FLYOUT_WITH_VULNERABILITY_PREVIEW);
@ -132,11 +134,13 @@ export const VulnerabilitiesPreview = ({
isRiskScoreExist,
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
hasNonClosedAlerts,
path: { tab: EntityDetailsLeftPanelTab.CSP_INSIGHTS, subTab: 'vulnerabilitiesTabId' },
},
});
}, [
hasMisconfigurationFindings,
hasNonClosedAlerts,
hasVulnerabilitiesFindings,
isRiskScoreExist,
name,

View file

@ -32,6 +32,7 @@ describe('useNavigateToAlertsPageWithFilters', () => {
expect(mockNavigateTo).toHaveBeenCalledWith({
deepLinkId: SecurityPageName.alerts,
path: "?pageFilters=!((exclude:!f,existsSelected:!f,fieldName:'test field',hideActionBar:!f,selectedOptions:!('test value'),title:'test filter'))",
openInNewTab: false,
});
});
@ -63,6 +64,7 @@ describe('useNavigateToAlertsPageWithFilters', () => {
expect(mockNavigateTo).toHaveBeenCalledWith({
deepLinkId: SecurityPageName.alerts,
path: "?pageFilters=!((exclude:!f,existsSelected:!f,fieldName:'test field 1',hideActionBar:!f,selectedOptions:!('test value 1'),title:'test filter 1'),(exclude:!t,existsSelected:!t,fieldName:'test field 2',hideActionBar:!t,selectedOptions:!('test value 2'),title:'test filter 2'))",
openInNewTab: false,
});
});

View file

@ -16,7 +16,7 @@ import { URL_PARAM_KEY } from './use_url_state';
export const useNavigateToAlertsPageWithFilters = () => {
const { navigateTo } = useNavigation();
return (filterItems: FilterControlConfig | FilterControlConfig[]) => {
return (filterItems: FilterControlConfig | FilterControlConfig[], openInNewTab = false) => {
const urlFilterParams = encode(
formatPageFilterSearchParam(Array.isArray(filterItems) ? filterItems : [filterItems])
);
@ -24,6 +24,7 @@ export const useNavigateToAlertsPageWithFilters = () => {
navigateTo({
deepLinkId: SecurityPageName.alerts,
path: `?${URL_PARAM_KEY.pageFilter}=${urlFilterParams}`,
openInNewTab,
});
};
};

View file

@ -25,6 +25,7 @@ export interface HostDetailsPanelProps extends Record<string, unknown> {
scopeId: string;
hasMisconfigurationFindings?: boolean;
hasVulnerabilitiesFindings?: boolean;
hasNonClosedAlerts?: boolean;
path?: {
tab?: EntityDetailsLeftPanelTab;
subTab?: CspInsightLeftPanelSubTab;
@ -43,6 +44,7 @@ export const HostDetailsPanel = ({
path,
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
hasNonClosedAlerts,
}: HostDetailsPanelProps) => {
const [selectedTabId, setSelectedTabId] = useState(
path?.tab === EntityDetailsLeftPanelTab.CSP_INSIGHTS
@ -58,11 +60,18 @@ export const HostDetailsPanel = ({
// Determine if the Insights tab should be included
const insightsTab =
hasMisconfigurationFindings || hasVulnerabilitiesFindings
hasMisconfigurationFindings || hasVulnerabilitiesFindings || hasNonClosedAlerts
? [getInsightsInputTab({ name, fieldName: 'host.name' })]
: [];
return [[...riskScoreTab, ...insightsTab], EntityDetailsLeftPanelTab.RISK_INPUTS, () => {}];
}, [isRiskScoreExist, name, scopeId, hasMisconfigurationFindings, hasVulnerabilitiesFindings]);
}, [
isRiskScoreExist,
name,
scopeId,
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
hasNonClosedAlerts,
]);
return (
<>

View file

@ -13,6 +13,8 @@ import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-commo
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 { sum } from 'lodash';
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 { useRefetchQueryById } from '../../../entity_analytics/api/hooks/use_refetch_query_by_id';
import { RISK_INPUTS_TAB_QUERY_ID } from '../../../entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab';
import type { Refetch } from '../../../common/types';
@ -35,6 +37,7 @@ import { useObservedHost } from './hooks/use_observed_host';
import { HostDetailsPanelKey } from '../host_details_left';
import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header';
import { HostPreviewPanelFooter } from '../host_preview/footer';
import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index';
import { EntityEventTypes } from '../../../common/lib/telemetry';
export interface HostPanelProps extends Record<string, unknown> {
@ -120,6 +123,21 @@ export const HostPanel = ({
const hasVulnerabilitiesFindings = sum(Object.values(vulnerabilitiesData?.count || {})) > 0;
const { signalIndexName } = useSignalIndex();
const entityFilter = useMemo(() => ({ field: 'host.name', value: hostName }), [hostName]);
const { items: alertsData } = useAlertsByStatus({
entityFilter,
signalIndexName,
queryId: `${DETECTION_RESPONSE_ALERTS_BY_STATUS_ID}HOST_NAME_RIGHT`,
to,
from,
});
const hasNonClosedAlerts =
(alertsData?.acknowledged?.total || 0) + (alertsData?.open?.total || 0) > 0;
useQueryInspector({
deleteQuery,
inspect: inspectRiskScore,
@ -144,6 +162,7 @@ export const HostPanel = ({
path: tab ? { tab } : undefined,
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
hasNonClosedAlerts,
},
});
},
@ -155,6 +174,7 @@ export const HostPanel = ({
isRiskScoreExist,
hasMisconfigurationFindings,
hasVulnerabilitiesFindings,
hasNonClosedAlerts,
]
);

View file

@ -28,6 +28,7 @@ export enum EntityDetailsLeftPanelTab {
export enum CspInsightLeftPanelSubTab {
MISCONFIGURATIONS = 'misconfigurationTabId',
VULNERABILITIES = 'vulnerabilitiesTabId',
ALERTS = 'alertsTabId',
}
export interface PanelHeaderProps {

View file

@ -29,6 +29,7 @@ export interface UserDetailsPanelProps extends Record<string, unknown> {
path?: PanelPath;
scopeId: string;
hasMisconfigurationFindings?: boolean;
hasNonClosedAlerts?: boolean;
}
export interface UserDetailsExpandableFlyoutProps extends FlyoutPanelProps {
key: 'user_details';
@ -42,6 +43,7 @@ export const UserDetailsPanel = ({
path,
scopeId,
hasMisconfigurationFindings,
hasNonClosedAlerts,
}: UserDetailsPanelProps) => {
const managedUser = useManagedUser(user.name, user.email);
const tabs = useTabs(
@ -49,7 +51,8 @@ export const UserDetailsPanel = ({
user.name,
isRiskScoreExist,
scopeId,
hasMisconfigurationFindings
hasMisconfigurationFindings,
hasNonClosedAlerts
);
const { selectedTabId, setSelectedTabId } = useSelectedTab(
@ -57,7 +60,8 @@ export const UserDetailsPanel = ({
user,
tabs,
path,
hasMisconfigurationFindings
hasMisconfigurationFindings,
hasNonClosedAlerts
);
if (managedUser.isLoading) return <FlyoutLoading />;
@ -83,7 +87,8 @@ const useSelectedTab = (
user: UserParam,
tabs: LeftPanelTabsType,
path: PanelPath | undefined,
hasMisconfigurationFindings?: boolean
hasMisconfigurationFindings?: boolean,
hasNonClosedAlerts?: boolean
) => {
const { openLeftPanel } = useExpandableFlyoutApi();
@ -101,6 +106,7 @@ const useSelectedTab = (
user,
isRiskScoreExist,
hasMisconfigurationFindings,
hasNonClosedAlerts,
path: {
tab: tabId,
},

View file

@ -30,7 +30,8 @@ export const useTabs = (
name: string,
isRiskScoreExist: boolean,
scopeId: string,
hasMisconfigurationFindings?: boolean
hasMisconfigurationFindings?: boolean,
hasNonClosedAlerts?: boolean
): LeftPanelTabsType =>
useMemo(() => {
const tabs: LeftPanelTabsType = [];
@ -55,12 +56,19 @@ export const useTabs = (
tabs.push(getEntraTab(entraManagedUser));
}
if (hasMisconfigurationFindings) {
if (hasMisconfigurationFindings || hasNonClosedAlerts) {
tabs.push(getInsightsInputTab({ name, fieldName: 'user.name' }));
}
return tabs;
}, [hasMisconfigurationFindings, isRiskScoreExist, managedUser, name, scopeId]);
}, [
hasMisconfigurationFindings,
hasNonClosedAlerts,
isRiskScoreExist,
managedUser,
name,
scopeId,
]);
const getOktaTab = (oktaManagedUser: ManagedUserHit) => ({
id: EntityDetailsLeftPanelTab.OKTA,

View file

@ -33,6 +33,9 @@ import { UserDetailsPanelKey } from '../user_details_left';
import { useObservedUser } from './hooks/use_observed_user';
import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header';
import { UserPreviewPanelFooter } from '../user_preview/footer';
import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index';
import { useAlertsByStatus } from '../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
import { DETECTION_RESPONSE_ALERTS_BY_STATUS_ID } from '../../../overview/components/detection_response/alerts_by_status/types';
import { EntityEventTypes } from '../../../common/lib/telemetry';
export interface UserPanelProps extends Record<string, unknown> {
@ -112,6 +115,21 @@ export const UserPanel = ({
const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0;
const { signalIndexName } = useSignalIndex();
const entityFilter = useMemo(() => ({ field: 'user.name', value: userName }), [userName]);
const { items: alertsData } = useAlertsByStatus({
entityFilter,
signalIndexName,
queryId: `${DETECTION_RESPONSE_ALERTS_BY_STATUS_ID}USER_NAME_RIGHT`,
to,
from,
});
const hasNonClosedAlerts =
(alertsData?.acknowledged?.total || 0) + (alertsData?.open?.total || 0) > 0;
useQueryInspector({
deleteQuery,
inspect,
@ -139,6 +157,7 @@ export const UserPanel = ({
},
path: tab ? { tab } : undefined,
hasMisconfigurationFindings,
hasNonClosedAlerts,
},
});
},
@ -150,6 +169,7 @@ export const UserPanel = ({
userName,
email,
hasMisconfigurationFindings,
hasNonClosedAlerts,
]
);
const openPanelFirstTab = useCallback(
@ -191,7 +211,8 @@ export const UserPanel = ({
<>
<FlyoutNavigation
flyoutIsExpandable={
!isPreviewMode && (hasUserDetailsData || hasMisconfigurationFindings)
!isPreviewMode &&
(hasUserDetailsData || hasMisconfigurationFindings || hasNonClosedAlerts)
}
expandDetails={openPanelFirstTab}
/>