diff --git a/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.test.ts b/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.test.ts index 0248cdf9b6e36..04cb76f4441c5 100644 --- a/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.test.ts +++ b/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.test.ts @@ -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)); + }); + }); }); diff --git a/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.ts b/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.ts index 7039c99af6d53..bd531fa63804f 100644 --- a/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.ts +++ b/x-pack/packages/kbn-cloud-security-posture/common/utils/helpers.ts @@ -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'], + }, + }, + ], + }, + }, + }; +}; diff --git a/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_findings.ts b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_findings.ts index 40880b132537d..9bbaedf587dde 100644 --- a/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_findings.ts +++ b/x-pack/packages/kbn-cloud-security-posture/public/src/hooks/use_misconfiguration_findings.ts @@ -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, diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx index e0199ab40168d..fff9450b6a1cb 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.test.tsx @@ -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( - + ); - expect(getByTestId('securitySolutionFlyoutInsightsAlertsTitleText')).toBeInTheDocument(); + expect(getByTestId('securitySolutionFlyoutInsightsAlertsTitleLink')).toBeInTheDocument(); }); it('renders correct alerts number', () => { const { getByTestId } = render( - + ); @@ -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( - + ); diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx index 3f9a0115d9ed1..c832f12c93f78 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx @@ -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: ( + + ), + } + : undefined, + [isPreviewMode, goToEntityInsightTab] + ); return ( ), + link: totalAlertsCount > 0 ? link : undefined, }} data-test-subj={'securitySolutionFlyoutInsightsAlerts'} > diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/alerts_findings_details_table.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/alerts_findings_details_table.tsx new file mode 100644 index 0000000000000..966de68e3497f --- /dev/null +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/alerts_findings_details_table.tsx @@ -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, 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) => { + 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> = [ + { + field: 'id', + name: '', + width: '5%', + render: (id: string, alert: ContextualFlyoutAlertsField) => ( + handleOnEventAlertDetailPanelOpened(id, alert.index, tableId)}> + + + ), + }, + { + field: 'ruleName', + render: (ruleName: string) => {ruleName}, + name: i18n.translate( + 'xpack.securitySolution.flyout.left.insights.alerts.table.ruleNameColumnName', + { + defaultMessage: 'Rule', + } + ), + width: '55%', + }, + { + field: 'severity', + render: (severity: AlertSeverity) => ( + + + + ), + name: i18n.translate( + 'xpack.securitySolution.flyout.left.insights.alerts.table.severityColumnName', + { + defaultMessage: 'Severity', + } + ), + width: '20%', + }, + { + field: 'status', + render: (status: string) => {capitalize(status)}, + 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 ( + <> + + openAlertsInAlertsPage()}> +

+ {i18n.translate('xpack.securitySolution.flyout.left.insights.alerts.tableTitle', { + defaultMessage: 'Alerts ', + })} + +

+
+ + + + + +
+ + ); + } +); + +AlertsDetailsTable.displayName = 'AlertsDetailsTable'; diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx index 05421cfa7a208..2e7b4171fd023 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx @@ -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: ( + + ), + '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( {activeInsightsId === CspInsightLeftPanelSubTab.MISCONFIGURATIONS ? ( - ) : ( + ) : activeInsightsId === CspInsightLeftPanelSubTab.VULNERABILITIES ? ( + ) : ( + )} ); diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/entity_insight.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/entity_insight.tsx index a43b56876f1ab..7139994f7e972 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/entity_insight.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/entity_insight.tsx @@ -94,7 +94,12 @@ export const EntityInsight = ({ if (alertsCount > 0) { insightContent.push( <> - + ); @@ -103,14 +108,23 @@ export const EntityInsight = ({ if (hasMisconfigurationFindings) insightContent.push( <> - + 0} + isPreviewMode={isPreviewMode} + /> ); if (isVulnerabilitiesFindingForHost && hasVulnerabilitiesFindings) insightContent.push( <> - + 0} + /> ); diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx index b133e9db22050..42a5906ce4e36 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx @@ -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, diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx index a9ddaff62085b..c4335d921e371 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/vulnerabilities/vulnerabilities_preview.tsx @@ -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, diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.test.ts index 6a957318f278f..3bfc0c56e81fa 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.test.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.test.ts @@ -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, }); }); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.ts b/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.ts index fffa65797b3f8..037bac32d8c92 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_navigate_to_alerts_page_with_filters.ts @@ -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, }); }; }; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx index 6e5774ba1756e..107cb83ddd97b 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx @@ -25,6 +25,7 @@ export interface HostDetailsPanelProps extends Record { 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 ( <> diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx index 83fa75474a1cc..a7e99898606f8 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx @@ -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 { @@ -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, ] ); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx index 08623c941ba67..254985b865840 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx @@ -28,6 +28,7 @@ export enum EntityDetailsLeftPanelTab { export enum CspInsightLeftPanelSubTab { MISCONFIGURATIONS = 'misconfigurationTabId', VULNERABILITIES = 'vulnerabilitiesTabId', + ALERTS = 'alertsTabId', } export interface PanelHeaderProps { diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx index 8e6cf3a9ee9d2..87c9e5abc7afd 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx @@ -29,6 +29,7 @@ export interface UserDetailsPanelProps extends Record { 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 ; @@ -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, }, diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx index 6f27b054759f2..0c1cdcaa904a9 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx @@ -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, diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx index 42c8664b2ac0c..07762ed9aea0c 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx @@ -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 { @@ -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 = ({ <>