[Cloud Security] Filters for Contextual Flyout Datagrid (#201708)

## Summary



https://github.com/user-attachments/assets/59ace35f-62b8-4c08-bf2c-eed200db791d

This PR is for adding Filters for Contextual Flytout Datagrid
This commit is contained in:
Rickyanto Ang 2024-12-02 09:26:03 -08:00 committed by GitHub
parent 08c3bc217f
commit 2f62cdebfc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 480 additions and 78 deletions

View file

@ -47,3 +47,8 @@ export const VULNERABILITIES_SEVERITY: Record<VulnSeverity, VulnSeverity> = {
CRITICAL: 'CRITICAL',
UNKNOWN: 'UNKNOWN',
};
export const MISCONFIGURATION_STATUS: Record<string, string> = {
PASSED: 'passed',
FAILED: 'failed',
};

View file

@ -28,7 +28,9 @@ export * from './constants';
export {
extractErrorMessage,
buildMutedRulesFilter,
buildEntityFlyoutPreviewQuery,
buildGenericEntityFlyoutPreviewQuery,
buildMisconfigurationEntityFlyoutPreviewQuery,
buildVulnerabilityEntityFlyoutPreviewQuery,
} from './utils/helpers';
export { getAbbreviatedNumber } from './utils/get_abbreviated_number';
export { UiMetricService } from './utils/ui_metrics';

View file

@ -9,8 +9,10 @@ import {
extractErrorMessage,
defaultErrorMessage,
buildMutedRulesFilter,
buildEntityFlyoutPreviewQuery,
buildEntityAlertsQuery,
buildGenericEntityFlyoutPreviewQuery,
buildMisconfigurationEntityFlyoutPreviewQuery,
buildVulnerabilityEntityFlyoutPreviewQuery,
} from './helpers';
const fallbackMessage = 'thisIsAFallBackMessage';
@ -145,7 +147,7 @@ describe('test helper methods', () => {
});
});
describe('buildEntityFlyoutPreviewQueryTest', () => {
describe('buildGenericEntityFlyoutPreviewQuery', () => {
it('should return the correct query when given field and query', () => {
const field = 'host.name';
const query = 'exampleHost';
@ -162,10 +164,10 @@ describe('test helper methods', () => {
},
};
expect(buildEntityFlyoutPreviewQuery(field, query)).toEqual(expectedQuery);
expect(buildGenericEntityFlyoutPreviewQuery(field, query)).toEqual(expectedQuery);
});
it('should return the correct query when given field and empty query', () => {
it('should return the correct query when given field and empty query and empty status', () => {
const field = 'host.name';
const expectedQuery = {
bool: {
@ -180,12 +182,143 @@ describe('test helper methods', () => {
},
};
expect(buildEntityFlyoutPreviewQuery(field)).toEqual(expectedQuery);
expect(buildGenericEntityFlyoutPreviewQuery(field)).toEqual(expectedQuery);
});
it('should return the correct query when given field and queryValue and status but empty queryField', () => {
const field = 'host.name';
const query = 'exampleHost';
const status = 'pass';
const expectedQuery = {
bool: {
filter: [
{
bool: {
should: [{ term: { 'host.name': 'exampleHost' } }],
minimum_should_match: 1,
},
},
],
},
};
expect(buildGenericEntityFlyoutPreviewQuery(field, query, status)).toEqual(expectedQuery);
});
it('should return the correct query when given field and queryValue and queryField but empty status', () => {
const field = 'host.name';
const query = 'exampleHost';
const emptyStatus = undefined;
const queryField = 'some.field';
const expectedQuery = {
bool: {
filter: [
{
bool: {
should: [{ term: { 'host.name': 'exampleHost' } }],
minimum_should_match: 1,
},
},
],
},
};
expect(buildGenericEntityFlyoutPreviewQuery(field, query, emptyStatus, queryField)).toEqual(
expectedQuery
);
});
it('should return the correct query when given all the parameters', () => {
const field = 'host.name';
const query = 'exampleHost';
const emptyStatus = 'some.status';
const queryField = 'some.field';
const expectedQuery = {
bool: {
filter: [
{
bool: {
should: [{ term: { 'host.name': 'exampleHost' } }],
minimum_should_match: 1,
},
},
{
bool: {
should: [{ term: { 'some.field': 'some.status' } }],
minimum_should_match: 1,
},
},
],
},
};
expect(buildGenericEntityFlyoutPreviewQuery(field, query, emptyStatus, queryField)).toEqual(
expectedQuery
);
});
});
describe('buildMisconfigurationEntityFlyoutPreviewQuery', () => {
it('should return the correct query when given field, queryValue, status and queryType Misconfiguration', () => {
const field = 'host.name';
const queryValue = 'exampleHost';
const status = 'pass';
const expectedQuery = {
bool: {
filter: [
{
bool: {
should: [{ term: { 'host.name': 'exampleHost' } }],
minimum_should_match: 1,
},
},
{
bool: {
should: [{ term: { 'result.evaluation': 'pass' } }],
minimum_should_match: 1,
},
},
],
},
};
expect(buildMisconfigurationEntityFlyoutPreviewQuery(field, queryValue, status)).toEqual(
expectedQuery
);
});
});
describe('buildVulnerabilityEntityFlyoutPreviewQuery', () => {
it('should return the correct query when given field, queryValue, status and queryType Vulnerability', () => {
const field = 'host.name';
const queryValue = 'exampleHost';
const status = 'low';
const expectedQuery = {
bool: {
filter: [
{
bool: {
should: [{ term: { 'host.name': 'exampleHost' } }],
minimum_should_match: 1,
},
},
{
bool: {
should: [{ term: { 'vulnerability.severity': 'low' } }],
minimum_should_match: 1,
},
},
],
},
};
expect(buildVulnerabilityEntityFlyoutPreviewQuery(field, queryValue, status)).toEqual(
expectedQuery
);
});
});
describe('buildEntityAlertsQuery', () => {
const getExpectedAlertsQuery = (size?: number) => {
const getExpectedAlertsQuery = (size?: number, severity?: string) => {
return {
size: size || 0,
_source: false,
@ -202,20 +335,30 @@ describe('test helper methods', () => {
filter: [
{
bool: {
must: [],
filter: [
should: [
{
match_phrase: {
'host.name': {
query: 'exampleHost',
},
term: {
'host.name': 'exampleHost',
},
},
],
should: [],
must_not: [],
minimum_should_match: 1,
},
},
severity
? {
bool: {
should: [
{
term: {
'kibana.alert.severity': 'low',
},
},
],
minimum_should_match: 1,
},
}
: undefined,
{
range: {
'@timestamp': {
@ -229,7 +372,7 @@ describe('test helper methods', () => {
'kibana.alert.workflow_status': ['open', 'acknowledged'],
},
},
],
].filter(Boolean),
},
},
};
@ -256,5 +399,18 @@ describe('test helper methods', () => {
expect(buildEntityAlertsQuery(field, to, from, query)).toEqual(getExpectedAlertsQuery(size));
});
it('should return the correct query when given severity query', () => {
const field = 'host.name';
const query = 'exampleHost';
const to = 'Tomorrow';
const from = 'Today';
const size = undefined;
const severity = 'low';
expect(buildEntityAlertsQuery(field, to, from, query, size, severity)).toEqual(
getExpectedAlertsQuery(size, 'low')
);
});
});
});

View file

@ -43,7 +43,12 @@ export const buildMutedRulesFilter = (
return mutedRulesFilterQuery;
};
export const buildEntityFlyoutPreviewQuery = (field: string, queryValue?: string) => {
export const buildGenericEntityFlyoutPreviewQuery = (
field: string,
queryValue?: string,
status?: string,
queryField?: string
) => {
return {
bool: {
filter: [
@ -59,17 +64,52 @@ export const buildEntityFlyoutPreviewQuery = (field: string, queryValue?: string
minimum_should_match: 1,
},
},
],
status && queryField
? {
bool: {
should: [
{
term: {
[queryField]: status,
},
},
],
minimum_should_match: 1,
},
}
: undefined,
].filter(Boolean),
},
};
};
// Higher-order function for Misconfiguration
export const buildMisconfigurationEntityFlyoutPreviewQuery = (
field: string,
queryValue?: string,
status?: string
) => {
const queryField = 'result.evaluation';
return buildGenericEntityFlyoutPreviewQuery(field, queryValue, status, queryField);
};
// Higher-order function for Vulnerability
export const buildVulnerabilityEntityFlyoutPreviewQuery = (
field: string,
queryValue?: string,
status?: string
) => {
const queryField = 'vulnerability.severity';
return buildGenericEntityFlyoutPreviewQuery(field, queryValue, status, queryField);
};
export const buildEntityAlertsQuery = (
field: string,
to: string,
from: string,
queryValue?: string,
size?: number
size?: number,
severity?: string
) => {
return {
size: size || 0,
@ -87,20 +127,30 @@ export const buildEntityAlertsQuery = (
filter: [
{
bool: {
must: [],
filter: [
should: [
{
match_phrase: {
[field]: {
query: queryValue,
},
term: {
[field]: `${queryValue || ''}`,
},
},
],
should: [],
must_not: [],
minimum_should_match: 1,
},
},
severity
? {
bool: {
should: [
{
term: {
'kibana.alert.severity': severity,
},
},
],
minimum_should_match: 1,
},
}
: undefined,
{
range: {
'@timestamp': {
@ -114,7 +164,7 @@ export const buildEntityAlertsQuery = (
'kibana.alert.workflow_status': ['open', 'acknowledged'],
},
},
],
].filter(Boolean),
},
},
};

View file

@ -11,3 +11,6 @@ export const statusColors = {
failed: euiThemeVars.euiColorVis9,
unknown: euiThemeVars.euiColorLightShade,
};
export const HOST_NAME = 'host.name';
export const USER_NAME = 'user.name';

View file

@ -5,12 +5,12 @@
* 2.0.
*/
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import { buildGenericEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import { useMisconfigurationPreview } from './use_misconfiguration_preview';
export const useHasMisconfigurations = (field: 'host.name' | 'user.name', value: string) => {
const { data } = useMisconfigurationPreview({
query: buildEntityFlyoutPreviewQuery(field, value),
query: buildGenericEntityFlyoutPreviewQuery(field, value),
sort: [],
enabled: true,
pageSize: 1,

View file

@ -5,13 +5,13 @@
* 2.0.
*/
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import { buildGenericEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import { useVulnerabilitiesPreview } from './use_vulnerabilities_preview';
import { hasVulnerabilitiesData } from '../utils/vulnerability_helpers';
export const useHasVulnerabilities = (field: 'host.name' | 'user.name', value: string) => {
const { data: vulnerabilitiesData } = useVulnerabilitiesPreview({
query: buildEntityFlyoutPreviewQuery(field, value),
query: buildGenericEntityFlyoutPreviewQuery(field, value),
sort: [],
enabled: true,
pageSize: 1,

View file

@ -58,7 +58,7 @@ export interface CspClientPluginStartDeps {
export interface CspBaseEsQuery {
query?: {
bool: {
filter: estypes.QueryDslQueryContainer[];
filter: Array<estypes.QueryDslQueryContainer | undefined> | undefined;
};
};
}

View file

@ -10,6 +10,7 @@ import { getVulnerabilityStats } from './vulnerability_helpers';
import { i18n } from '@kbn/i18n';
describe('getVulnerabilitiesAggregationCount', () => {
const mockFilterFunction = jest.fn();
it('should return empty array when all severity count is 0', () => {
const result = getVulnerabilityStats({ critical: 0, high: 0, medium: 0, low: 0, none: 0 });
expect(result).toEqual([]);
@ -17,8 +18,12 @@ describe('getVulnerabilitiesAggregationCount', () => {
it('should return stats for low, medium, high, and critical vulnerabilities', () => {
const result = getVulnerabilityStats({ critical: 1, high: 2, medium: 3, low: 4, none: 5 });
const resultWithoutFunctions = result.map((item) => {
const { filter, reset, ...rest } = item;
return rest;
});
expect(result).toEqual([
expect(resultWithoutFunctions).toEqual([
{
key: i18n.translate(
'xpack.securitySolution.flyout.right.insights.vulnerabilities.noneVulnerabilitiesText',
@ -28,6 +33,7 @@ describe('getVulnerabilitiesAggregationCount', () => {
),
count: 5,
color: '#aaa',
isCurrentFilter: false,
},
{
key: i18n.translate(
@ -38,6 +44,7 @@ describe('getVulnerabilitiesAggregationCount', () => {
),
count: 4,
color: euiThemeVars.euiColorVis0,
isCurrentFilter: false,
},
{
key: i18n.translate(
@ -48,6 +55,7 @@ describe('getVulnerabilitiesAggregationCount', () => {
),
count: 3,
color: euiThemeVars.euiColorVis5_behindText,
isCurrentFilter: false,
},
{
key: i18n.translate(
@ -58,6 +66,7 @@ describe('getVulnerabilitiesAggregationCount', () => {
),
count: 2,
color: euiThemeVars.euiColorVis9_behindText,
isCurrentFilter: false,
},
{
key: i18n.translate(
@ -68,7 +77,43 @@ describe('getVulnerabilitiesAggregationCount', () => {
),
count: 1,
color: euiThemeVars.euiColorDanger,
isCurrentFilter: false,
},
]);
});
it('should return correct stats with correct onClick functions', () => {
const result = getVulnerabilityStats(
{ critical: 1, high: 2, medium: 3, low: 4, none: 5 },
mockFilterFunction
);
const event = { stopPropagation: jest.fn() } as unknown as React.MouseEvent<
SVGElement,
MouseEvent
>;
result[1].filter?.();
expect(mockFilterFunction).toHaveBeenCalledWith('LOW');
result[1].reset?.(event);
expect(mockFilterFunction).toHaveBeenCalledWith('');
});
it('should identify correct currentFilter', () => {
const currentFilter = 'LOW';
const result = getVulnerabilityStats(
{ critical: 1, high: 2, medium: 3, low: 4, none: 5 },
mockFilterFunction,
currentFilter
);
// Make sure that Low is set to Current Filter
expect(result[1].isCurrentFilter).toBe(true);
expect(result[1].key).toBe('Low');
// Make sure only Low is set as Current Filter and no other severity
result.forEach((item) => {
if (item.key !== 'Low') {
expect(item.isCurrentFilter).toBe(false);
}
});
});
});

View file

@ -14,6 +14,9 @@ interface VulnerabilitiesDistributionBarProps {
key: string;
count: number;
color: string;
isCurrentFilter?: boolean;
filter?: () => void;
reset?: (event: any) => void;
}
interface VulnerabilityCounts {
@ -30,7 +33,9 @@ export const hasVulnerabilitiesData = (counts: VulnerabilityCounts): boolean =>
};
export const getVulnerabilityStats = (
counts: VulnerabilityCounts
counts: VulnerabilityCounts,
filterFunction?: (filter: string) => void,
currentFilter?: string
): VulnerabilitiesDistributionBarProps[] => {
const vulnerabilityStats: VulnerabilitiesDistributionBarProps[] = [];
@ -50,6 +55,14 @@ export const getVulnerabilityStats = (
),
count: counts.none,
color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.UNKNOWN),
filter: () => {
filterFunction?.(VULNERABILITIES_SEVERITY.UNKNOWN);
},
isCurrentFilter: currentFilter === VULNERABILITIES_SEVERITY.UNKNOWN,
reset: (event: React.MouseEvent<SVGElement, MouseEvent>) => {
filterFunction?.('');
event?.stopPropagation();
},
});
if (counts.low > 0)
vulnerabilityStats.push({
@ -61,6 +74,14 @@ export const getVulnerabilityStats = (
),
count: counts.low,
color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.LOW),
filter: () => {
filterFunction?.(VULNERABILITIES_SEVERITY.LOW);
},
isCurrentFilter: currentFilter === VULNERABILITIES_SEVERITY.LOW,
reset: (event: React.MouseEvent<SVGElement, MouseEvent>) => {
filterFunction?.('');
event?.stopPropagation();
},
});
if (counts.medium > 0)
@ -73,6 +94,14 @@ export const getVulnerabilityStats = (
),
count: counts.medium,
color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.MEDIUM),
filter: () => {
filterFunction?.(VULNERABILITIES_SEVERITY.MEDIUM);
},
isCurrentFilter: currentFilter === VULNERABILITIES_SEVERITY.MEDIUM,
reset: (event: React.MouseEvent<SVGElement, MouseEvent>) => {
filterFunction?.('');
event?.stopPropagation();
},
});
if (counts.high > 0)
vulnerabilityStats.push({
@ -84,6 +113,14 @@ export const getVulnerabilityStats = (
),
count: counts.high,
color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.HIGH),
filter: () => {
filterFunction?.(VULNERABILITIES_SEVERITY.HIGH);
},
isCurrentFilter: currentFilter === VULNERABILITIES_SEVERITY.HIGH,
reset: (event: React.MouseEvent<SVGElement, MouseEvent>) => {
filterFunction?.('');
event?.stopPropagation();
},
});
if (counts.critical > 0)
vulnerabilityStats.push({
@ -95,6 +132,14 @@ export const getVulnerabilityStats = (
),
count: counts.critical,
color: getSeverityStatusColor(VULNERABILITIES_SEVERITY.CRITICAL),
filter: () => {
filterFunction?.(VULNERABILITIES_SEVERITY.CRITICAL);
},
isCurrentFilter: currentFilter === VULNERABILITIES_SEVERITY.CRITICAL,
reset: (event: React.MouseEvent<SVGElement, MouseEvent>) => {
filterFunction?.('');
event?.stopPropagation();
},
});
return vulnerabilityStats;

View file

@ -12,7 +12,15 @@ import { css } from '@emotion/react';
/** DistributionBar component props */
export interface DistributionBarProps {
/** distribution data points */
stats: Array<{ key: string; count: number; color: string; label?: React.ReactNode }>;
stats: Array<{
key: string;
count: number;
color: string;
label?: React.ReactNode;
isCurrentFilter?: boolean;
filter?: () => void;
reset?: (event: React.MouseEvent<SVGElement, MouseEvent>) => void;
}>;
/** hide the label above the bar at first render */
hideLastTooltip?: boolean;
/** data-test-subj used for querying the component in tests */
@ -156,7 +164,19 @@ export const DistributionBar: React.FC<DistributionBarProps> = React.memo(functi
const prettyNumber = numeral(stat.count).format('0,0a');
return (
<div key={stat.key} css={partStyle} data-test-subj={`${dataTestSubj}__part`}>
<div
key={stat.key}
css={partStyle}
data-test-subj={`${dataTestSubj}__part`}
onClick={stat.filter}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
stat.filter?.();
}
}}
tabIndex={0}
role="button"
>
<div css={styles.tooltip}>
<EuiFlexGroup
gutterSize={'none'}
@ -176,6 +196,11 @@ export const DistributionBar: React.FC<DistributionBarProps> = React.memo(functi
<EuiIcon type={'dot'} size={'s'} color={stat.color} />
</EuiFlexItem>
<EuiFlexItem grow={false}>{stat.label ? stat.label : stat.key}</EuiFlexItem>
{stat.isCurrentFilter ? (
<EuiFlexItem grow={false}>
<EuiIcon type="cross" size="m" onClick={stat.reset} />
</EuiFlexItem>
) : undefined}
</EuiFlexGroup>
</EuiBadge>
</EuiFlexItem>

View file

@ -19,6 +19,8 @@ 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 type { AlertsByStatus } 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 {
OPEN_IN_ALERTS_TITLE_HOSTNAME,
OPEN_IN_ALERTS_TITLE_STATUS,
@ -34,6 +36,7 @@ import { getSeverityColor } from '../../../detections/components/alerts_kpis/sev
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';
import { useNonClosedAlerts } from '../../hooks/use_non_closed_alerts';
type AlertSeverity = 'low' | 'medium' | 'high' | 'critical';
@ -68,6 +71,8 @@ export const AlertsDetailsTable = memo(
);
}, []);
const [currentFilter, setCurrentFilter] = useState<string>('');
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
@ -89,12 +94,46 @@ export const AlertsDetailsTable = memo(
const { to, from } = useGlobalTime();
const { signalIndexName } = useSignalIndex();
const { data } = useQueryAlerts({
query: buildEntityAlertsQuery(field, to, from, value, 500),
const { data, setQuery } = useQueryAlerts({
query: buildEntityAlertsQuery(field, to, from, value, 500, ''),
queryName: ALERTS_QUERY_NAMES.BY_RULE_BY_STATUS,
indexName: signalIndexName,
});
const { filteredAlertsData: alertsData } = useNonClosedAlerts({
field,
value,
to,
from,
queryId: `${DETECTION_RESPONSE_ALERTS_BY_STATUS_ID}`,
});
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),
filter: () => {
setCurrentFilter(key);
setQuery(buildEntityAlertsQuery(field, to, from, value, 500, key));
},
isCurrentFilter: currentFilter === key,
reset: (event: React.MouseEvent<SVGElement, MouseEvent>) => {
setCurrentFilter('');
setQuery(buildEntityAlertsQuery(field, to, from, value, 500, ''));
event?.stopPropagation();
},
}));
const alertDataResults = (data?.hits?.hits as AlertsDetailsFields[])?.map(
(item: AlertsDetailsFields) => {
return {
@ -108,19 +147,6 @@ export const AlertsDetailsTable = memo(
}
);
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 = {

View file

@ -10,8 +10,12 @@ import type { Criteria, EuiBasicTableColumn } from '@elastic/eui';
import { EuiSpacer, EuiPanel, EuiText, EuiBasicTable, EuiIcon } from '@elastic/eui';
import { useMisconfigurationFindings } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_findings';
import { i18n } from '@kbn/i18n';
import type { CspFinding, CspFindingResult } from '@kbn/cloud-security-posture-common';
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import {
MISCONFIGURATION_STATUS,
type CspFinding,
type CspFindingResult,
buildMisconfigurationEntityFlyoutPreviewQuery,
} from '@kbn/cloud-security-posture-common';
import { euiThemeVars } from '@kbn/ui-theme';
import { DistributionBar } from '@kbn/security-solution-distribution-bar';
import type { CspBenchmarkRuleMetadata } from '@kbn/cloud-security-posture-common/schema/rules/latest';
@ -25,11 +29,17 @@ import {
import { METRIC_TYPE } from '@kbn/analytics';
import { useGetNavigationUrlParams } from '@kbn/cloud-security-posture/src/hooks/use_get_navigation_url_params';
import { SecurityPageName } from '@kbn/deeplinks-security';
import { useHasMisconfigurations } from '@kbn/cloud-security-posture/src/hooks/use_has_misconfigurations';
import { SecuritySolutionLinkAnchor } from '../../../common/components/links';
type MisconfigurationFindingDetailFields = Pick<CspFinding, 'result' | 'rule' | 'resource'>;
const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => {
const getFindingsStats = (
passedFindingsStats: number,
failedFindingsStats: number,
filterFunction: (filter: string) => void,
currentFilter: string
) => {
if (passedFindingsStats === 0 && failedFindingsStats === 0) return [];
return [
{
@ -41,6 +51,14 @@ const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: numb
),
count: passedFindingsStats,
color: euiThemeVars.euiColorSuccess,
filter: () => {
filterFunction(MISCONFIGURATION_STATUS.PASSED);
},
isCurrentFilter: currentFilter === MISCONFIGURATION_STATUS.PASSED,
reset: (event: React.MouseEvent<SVGElement, MouseEvent>) => {
filterFunction('');
event?.stopPropagation();
},
},
{
key: i18n.translate(
@ -51,6 +69,14 @@ const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: numb
),
count: failedFindingsStats,
color: euiThemeVars.euiColorVis9,
filter: () => {
filterFunction(MISCONFIGURATION_STATUS.FAILED);
},
isCurrentFilter: currentFilter === MISCONFIGURATION_STATUS.FAILED,
reset: (event: React.MouseEvent<SVGElement, MouseEvent>) => {
filterFunction('');
event?.stopPropagation();
},
},
];
};
@ -67,15 +93,16 @@ export const MisconfigurationFindingsDetailsTable = memo(
);
}, []);
const [currentFilter, setCurrentFilter] = useState<string>('');
const { data } = useMisconfigurationFindings({
query: buildEntityFlyoutPreviewQuery(field, value),
query: buildMisconfigurationEntityFlyoutPreviewQuery(field, value, currentFilter),
sort: [],
enabled: true,
pageSize: 1,
});
const passedFindings = data?.count.passed || 0;
const failedFindings = data?.count.failed || 0;
const { passedFindings, failedFindings } = useHasMisconfigurations(field, value);
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
@ -129,6 +156,13 @@ export const MisconfigurationFindingsDetailsTable = memo(
const linkWidth = 40;
const resultWidth = 74;
const misconfgurationStats = getFindingsStats(
passedFindings,
failedFindings,
setCurrentFilter,
currentFilter
);
const columns: Array<EuiBasicTableColumn<MisconfigurationFindingDetailFields>> = [
{
field: 'rule',
@ -202,7 +236,7 @@ export const MisconfigurationFindingsDetailsTable = memo(
<EuiIcon type={'popout'} />
</SecuritySolutionLinkAnchor>
<EuiSpacer size="xl" />
<DistributionBar stats={getFindingsStats(passedFindings, failedFindings)} />
<DistributionBar stats={misconfgurationStats} />
<EuiSpacer size="l" />
<EuiBasicTable
items={pageOfItems || []}

View file

@ -9,8 +9,10 @@ import React, { memo, useEffect, useState } from 'react';
import type { Criteria, EuiBasicTableColumn } from '@elastic/eui';
import { EuiSpacer, EuiPanel, EuiText, EuiBasicTable, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { VulnSeverity } from '@kbn/cloud-security-posture-common';
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import {
buildVulnerabilityEntityFlyoutPreviewQuery,
type VulnSeverity,
} from '@kbn/cloud-security-posture-common';
import { DistributionBar } from '@kbn/security-solution-distribution-bar';
import { useVulnerabilitiesFindings } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_findings';
import type {
@ -30,6 +32,7 @@ import {
import { METRIC_TYPE } from '@kbn/analytics';
import { SecurityPageName } from '@kbn/deeplinks-security';
import { useGetNavigationUrlParams } from '@kbn/cloud-security-posture/src/hooks/use_get_navigation_url_params';
import { useHasVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_has_vulnerabilities';
import { SecuritySolutionLinkAnchor } from '../../../common/components/links';
type VulnerabilitiesFindingDetailFields = Pick<
@ -52,14 +55,18 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
);
}, []);
const [currentFilter, setCurrentFilter] = useState<string>('');
const { data } = useVulnerabilitiesFindings({
query: buildEntityFlyoutPreviewQuery('host.name', value),
query: buildVulnerabilityEntityFlyoutPreviewQuery('host.name', value, currentFilter),
sort: [],
enabled: true,
pageSize: 1,
});
const { CRITICAL = 0, HIGH = 0, MEDIUM = 0, LOW = 0, NONE = 0 } = data?.count || {};
const { counts } = useHasVulnerabilities('host.name', value);
const { critical = 0, high = 0, medium = 0, low = 0, none = 0 } = counts || {};
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
@ -120,6 +127,18 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
);
};
const vulnerabilityStats = getVulnerabilityStats(
{
critical,
high,
medium,
low,
none,
},
setCurrentFilter,
currentFilter
);
const columns: Array<EuiBasicTableColumn<VulnerabilitiesFindingDetailFields>> = [
{
field: 'vulnerability',
@ -220,15 +239,7 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
<EuiIcon type={'popout'} />
</SecuritySolutionLinkAnchor>
<EuiSpacer size="xl" />
<DistributionBar
stats={getVulnerabilityStats({
critical: CRITICAL,
high: HIGH,
medium: MEDIUM,
low: LOW,
none: NONE,
})}
/>
<DistributionBar stats={vulnerabilityStats} />
<EuiSpacer size="l" />
<EuiBasicTable
items={pageOfItems || []}

View file

@ -13,7 +13,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { DistributionBar } from '@kbn/security-solution-distribution-bar';
import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
import {
buildEntityFlyoutPreviewQuery,
buildGenericEntityFlyoutPreviewQuery,
getAbbreviatedNumber,
} from '@kbn/cloud-security-posture-common';
import { getVulnerabilityStats, hasVulnerabilitiesData } from '@kbn/cloud-security-posture';
@ -73,7 +73,7 @@ export const VulnerabilitiesPreview = ({
}, []);
const { data } = useVulnerabilitiesPreview({
query: buildEntityFlyoutPreviewQuery(field, value),
query: buildGenericEntityFlyoutPreviewQuery(field, value),
sort: [],
enabled: true,
pageSize: 1,

View file

@ -10,7 +10,7 @@ import { EuiFlexItem, type EuiFlexGroupProps, useEuiTheme } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/css';
import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview';
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import { buildGenericEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import {
MISCONFIGURATION_INSIGHT,
uiMetricService,
@ -58,7 +58,7 @@ export const MisconfigurationsInsight: React.FC<MisconfigurationsInsightProps> =
const { scopeId, isPreview } = useDocumentDetailsContext();
const { euiTheme } = useEuiTheme();
const { data } = useMisconfigurationPreview({
query: buildEntityFlyoutPreviewQuery(fieldName, name),
query: buildGenericEntityFlyoutPreviewQuery(fieldName, name),
sort: [],
enabled: true,
pageSize: 1,

View file

@ -10,7 +10,7 @@ import { EuiFlexItem, type EuiFlexGroupProps, useEuiTheme } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/css';
import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview';
import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import { buildGenericEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
import { getVulnerabilityStats, hasVulnerabilitiesData } from '@kbn/cloud-security-posture';
import {
uiMetricService,
@ -53,7 +53,7 @@ export const VulnerabilitiesInsight: React.FC<VulnerabilitiesInsightProps> = ({
const { scopeId, isPreview } = useDocumentDetailsContext();
const { euiTheme } = useEuiTheme();
const { data } = useVulnerabilitiesPreview({
query: buildEntityFlyoutPreviewQuery('host.name', hostName),
query: buildGenericEntityFlyoutPreviewQuery('host.name', hostName),
sort: [],
enabled: true,
pageSize: 1,