[Cloud Security] Contextual flyout table sorting (#203950)

## Summary

This PR adds sorting capability to Alerts, Misconfiguration and
Vulnerabilities table


https://github.com/user-attachments/assets/12cc203e-470c-4166-bb8a-e2b64f4141e3
This commit is contained in:
Rickyanto Ang 2025-01-03 08:52:50 -08:00 committed by GitHub
parent fe988fa552
commit 9933cc6595
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 337 additions and 115 deletions

View file

@ -318,8 +318,18 @@ describe('test helper methods', () => {
});
describe('buildEntityAlertsQuery', () => {
const getExpectedAlertsQuery = (size?: number, severity?: string) => {
const field: 'host.name' | 'user.name' = 'host.name';
const query = 'exampleHost';
const to = 'Tomorrow';
const from = 'Today';
const getExpectedAlertsQuery = (
size?: number,
severity?: string,
sortField?: string,
sortDirection?: 'asc' | 'desc'
) => {
return {
sort: sortField ? [{ [sortField]: sortDirection }] : [],
size: size || 0,
_source: false,
fields: [
@ -379,37 +389,64 @@ describe('test helper methods', () => {
};
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;
const testObjectParams = {
field,
to,
from,
queryValue: query,
size,
};
expect(buildEntityAlertsQuery(field, to, from, query, size)).toEqual(
getExpectedAlertsQuery(size)
);
expect(buildEntityAlertsQuery(testObjectParams)).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;
const testObjectParams = {
field,
to,
from,
queryValue: query,
size,
};
expect(buildEntityAlertsQuery(field, to, from, query)).toEqual(getExpectedAlertsQuery(size));
expect(buildEntityAlertsQuery(testObjectParams)).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';
const testObjectParams = {
field,
to,
from,
queryValue: query,
size,
severity,
};
expect(buildEntityAlertsQuery(field, to, from, query, size, severity)).toEqual(
getExpectedAlertsQuery(size, 'low')
expect(buildEntityAlertsQuery(testObjectParams)).toEqual(getExpectedAlertsQuery(size, 'low'));
});
it('should return the correct query when given sort parameter', () => {
const size = undefined;
const severity = 'low';
const sortField = 'sort.field';
const sortDirection = 'asc';
const testObjectParams = {
field,
to,
from,
queryValue: query,
size,
severity,
sortField,
sortDirection,
};
expect(buildEntityAlertsQuery(testObjectParams)).toEqual(
getExpectedAlertsQuery(size, 'low', sortField, sortDirection)
);
});
});

View file

@ -9,6 +9,17 @@ import { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types';
import { i18n } from '@kbn/i18n';
import type { CspBenchmarkRulesStates } from '../schema/rules/latest';
interface BuildEntityAlertsQueryParams {
field: 'user.name' | 'host.name';
to: string;
from: string;
queryValue?: string;
size?: number;
severity?: string;
sortField?: string;
sortDirection?: string;
}
export const defaultErrorMessage = i18n.translate(
'sharedPlatformPackages.csp.common.utils.helpers.unknownError',
{
@ -106,17 +117,20 @@ export const buildVulnerabilityEntityFlyoutPreviewQuery = (
return buildGenericEntityFlyoutPreviewQuery(field, queryValue, status, queryField);
};
export const buildEntityAlertsQuery = (
field: string,
to: string,
from: string,
queryValue?: string,
size?: number,
severity?: string
) => {
export const buildEntityAlertsQuery = ({
field,
to,
from,
queryValue = '',
size = 0,
severity,
sortField,
sortDirection,
}: BuildEntityAlertsQueryParams) => {
return {
size: size || 0,
_source: false,
sort: sortField ? [{ [sortField]: sortDirection }] : [],
fields: [
'_id',
'_index',

View file

@ -23,6 +23,18 @@ import {
getMisconfigurationAggregationCount,
} from '../utils/hooks_utils';
export enum MISCONFIGURATION {
RESULT_EVALUATION = 'result.evaluation',
RULE_NAME = 'rule.name',
}
export interface MisconfigurationFindingTableDetailsFields {
[MISCONFIGURATION.RESULT_EVALUATION]: string;
[MISCONFIGURATION.RULE_NAME]: string;
}
export type MisconfigurationFindingDetailFields = Pick<CspFinding, 'rule' | 'resource'> &
MisconfigurationFindingTableDetailsFields;
export const useMisconfigurationFindings = (options: UseCspOptions) => {
const {
data,
@ -50,10 +62,11 @@ export const useMisconfigurationFindings = (options: UseCspOptions) => {
return {
count: getMisconfigurationAggregationCount(aggregations?.count.buckets),
rows: hits.hits.map((finding) => ({
result: finding._source?.result,
rule: finding?._source?.rule,
resource: finding?._source?.resource,
})) as Array<Pick<CspFinding, 'result' | 'rule' | 'resource'>>,
[MISCONFIGURATION.RULE_NAME]: finding?._source?.rule?.name,
[MISCONFIGURATION.RESULT_EVALUATION]: finding._source?.result?.evaluation,
})) as MisconfigurationFindingDetailFields[],
};
},
{

View file

@ -14,17 +14,43 @@ import {
AggregationsMultiBucketAggregateBase,
AggregationsStringRareTermsBucketKeys,
} from '@elastic/elasticsearch/lib/api/types';
import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest';
import type {
CspVulnerabilityFinding,
Vulnerability,
} from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest';
import type { CoreStart } from '@kbn/core/public';
import type { CspClientPluginStartDeps, UseCspOptions } from '../types';
import { showErrorToast } from '../..';
import { getVulnerabilitiesAggregationCount, getVulnerabilitiesQuery } from '../utils/hooks_utils';
export enum VULNERABILITY {
ID = 'vulnerability.id',
SEVERITY = 'vulnerability.severity',
PACKAGE_NAME = 'vulnerability.package.name',
}
type LatestFindingsRequest = IKibanaSearchRequest<SearchRequest>;
type LatestFindingsResponse = IKibanaSearchResponse<
SearchResponse<CspVulnerabilityFinding, FindingsAggs>
>;
export interface VulnerabilitiesPackage extends Vulnerability {
package: {
name: string;
version: string;
};
}
export interface VulnerabilitiesFindingTableDetailsFields {
[VULNERABILITY.ID]: string;
[VULNERABILITY.SEVERITY]: string;
[VULNERABILITY.PACKAGE_NAME]: string;
}
export type VulnerabilitiesFindingDetailFields = Pick<Vulnerability, 'score'> &
Pick<CspVulnerabilityFinding, 'vulnerability' | 'resource'> &
VulnerabilitiesFindingTableDetailsFields;
interface FindingsAggs {
count: AggregationsMultiBucketAggregateBase<AggregationsStringRareTermsBucketKeys>;
}
@ -56,7 +82,11 @@ export const useVulnerabilitiesFindings = (options: UseCspOptions) => {
rows: hits.hits.map((finding) => ({
vulnerability: finding._source?.vulnerability,
resource: finding._source?.resource,
})) as Array<Pick<CspVulnerabilityFinding, 'vulnerability' | 'resource'>>,
score: finding._source?.vulnerability?.score,
[VULNERABILITY.ID]: finding._source?.vulnerability?.id,
[VULNERABILITY.SEVERITY]: finding._source?.vulnerability?.severity,
[VULNERABILITY.PACKAGE_NAME]: finding._source?.package?.name,
})) as VulnerabilitiesFindingDetailFields[],
};
},
{

View file

@ -64,7 +64,9 @@ export interface CspBaseEsQuery {
}
export interface UseCspOptions extends CspBaseEsQuery {
sort: string[][];
sort: Array<{
[key: string]: string;
}>;
enabled: boolean;
pageSize: number;
ignore_unavailable?: boolean;

View file

@ -73,12 +73,11 @@ export const getMisconfigurationAggregationCount = (
};
export const buildMisconfigurationsFindingsQuery = (
{ query }: UseCspOptions,
{ query, sort }: UseCspOptions,
rulesStates: CspBenchmarkRulesStates,
isPreview = false
) => {
const mutedRulesFilterQuery = buildMutedRulesFilter(rulesStates);
return {
index: CDR_MISCONFIGURATIONS_INDEX_PATTERN,
size: isPreview ? 0 : 500,
@ -86,6 +85,7 @@ export const buildMisconfigurationsFindingsQuery = (
ignore_unavailable: true,
query: buildMisconfigurationsFindingsQueryWithFilters(query, mutedRulesFilterQuery),
_source: MISCONFIGURATIONS_SOURCE_FIELDS,
sort,
};
};
@ -163,12 +163,13 @@ export const getFindingsCountAggQueryVulnerabilities = () => ({
},
});
export const getVulnerabilitiesQuery = ({ query }: UseCspOptions, isPreview = false) => ({
export const getVulnerabilitiesQuery = ({ query, sort }: UseCspOptions, isPreview = false) => ({
index: CDR_VULNERABILITIES_INDEX_PATTERN,
size: isPreview ? 0 : 500,
aggs: getFindingsCountAggQueryVulnerabilities(),
ignore_unavailable: true,
query: buildVulnerabilityFindingsQueryWithFilters(query),
sort,
});
const buildVulnerabilityFindingsQueryWithFilters = (query: UseCspOptions['query']) => {

View file

@ -7,7 +7,7 @@
import React, { memo, useCallback, useEffect, useState } from 'react';
import { capitalize } from 'lodash';
import type { Criteria, EuiBasicTableColumn } from '@elastic/eui';
import type { Criteria, EuiBasicTableColumn, EuiTableSortingType } 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';
@ -38,24 +38,35 @@ import { ALERT_PREVIEW_BANNER } from '../../../flyout/document_details/preview/c
import { FILTER_OPEN, FILTER_ACKNOWLEDGED } from '../../../../common/types';
import { useNonClosedAlerts } from '../../hooks/use_non_closed_alerts';
enum KIBANA_ALERTS {
SEVERITY = 'kibana.alert.severity',
RULE_NAME = 'kibana.alert.rule.name',
WORKFLOW_STATUS = 'kibana.alert.workflow_status',
}
type AlertSeverity = 'low' | 'medium' | 'high' | 'critical';
type AlertsSortFieldType =
| 'id'
| 'index'
| KIBANA_ALERTS.SEVERITY
| KIBANA_ALERTS.WORKFLOW_STATUS
| KIBANA_ALERTS.RULE_NAME;
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[];
[KIBANA_ALERTS.SEVERITY]: AlertSeverity[];
[KIBANA_ALERTS.RULE_NAME]: string[];
[KIBANA_ALERTS.WORKFLOW_STATUS]: string[];
}
interface ContextualFlyoutAlertsField {
id: string;
index: string;
ruleUuid: string;
ruleName: string;
severity: AlertSeverity;
status: string;
[KIBANA_ALERTS.SEVERITY]: AlertSeverity;
[KIBANA_ALERTS.RULE_NAME]: string;
[KIBANA_ALERTS.WORKFLOW_STATUS]: string;
}
interface AlertsDetailsFields {
@ -76,6 +87,16 @@ export const AlertsDetailsTable = memo(
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [sortField, setSortField] = useState<AlertsSortFieldType>(KIBANA_ALERTS.SEVERITY);
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const sorting: EuiTableSortingType<ContextualFlyoutAlertsField> = {
sort: {
field: sortField,
direction: sortDirection,
},
};
const alertsPagination = (alerts: ContextualFlyoutAlertsField[]) => {
let pageOfItems;
@ -95,7 +116,16 @@ export const AlertsDetailsTable = memo(
const { to, from } = useGlobalTime();
const { signalIndexName } = useSignalIndex();
const { data, setQuery } = useQueryAlerts({
query: buildEntityAlertsQuery(field, to, from, value, 500, ''),
query: buildEntityAlertsQuery({
field,
to,
from,
queryValue: value,
size: 500,
severity: '',
sortField,
sortDirection,
}),
queryName: ALERTS_QUERY_NAMES.BY_RULE_BY_STATUS,
indexName: signalIndexName,
});
@ -124,12 +154,32 @@ export const AlertsDetailsTable = memo(
color: getSeverityColor(key),
filter: () => {
setCurrentFilter(key);
setQuery(buildEntityAlertsQuery(field, to, from, value, 500, key));
setQuery(
buildEntityAlertsQuery({
field,
to,
from,
queryValue: value,
size: 500,
severity: key,
sortField,
sortDirection,
})
);
},
isCurrentFilter: currentFilter === key,
reset: (event: React.MouseEvent<SVGElement, MouseEvent>) => {
setCurrentFilter('');
setQuery(buildEntityAlertsQuery(field, to, from, value, 500, ''));
setQuery(
buildEntityAlertsQuery({
field,
to,
from,
queryValue: value,
size: 500,
severity: '',
})
);
event?.stopPropagation();
},
}));
@ -139,10 +189,9 @@ export const AlertsDetailsTable = memo(
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],
[KIBANA_ALERTS.RULE_NAME]: item.fields?.[KIBANA_ALERTS.RULE_NAME]?.[0],
[KIBANA_ALERTS.SEVERITY]: item.fields?.[KIBANA_ALERTS.SEVERITY]?.[0],
[KIBANA_ALERTS.WORKFLOW_STATUS]: item.fields?.[KIBANA_ALERTS.WORKFLOW_STATUS]?.[0],
};
}
);
@ -156,13 +205,34 @@ export const AlertsDetailsTable = memo(
pageSizeOptions: [10, 25, 100],
};
const onTableChange = ({ page }: Criteria<ContextualFlyoutAlertsField>) => {
if (page) {
const { index, size } = page;
setPageIndex(index);
setPageSize(size);
}
};
const onTableChange = useCallback(
({ page, sort }: Criteria<ContextualFlyoutAlertsField>) => {
if (page) {
const { index, size } = page;
setPageIndex(index);
setPageSize(size);
}
if (sort) {
const { field: fieldSort, direction } = sort;
setSortField(fieldSort);
setSortDirection(direction);
setQuery(
buildEntityAlertsQuery({
field,
to,
from,
queryValue: value,
size: 500,
severity: currentFilter,
sortField: fieldSort,
sortDirection: direction,
})
);
}
},
[currentFilter, field, from, setQuery, to, value]
);
const { openPreviewPanel } = useExpandableFlyoutApi();
@ -196,7 +266,7 @@ export const AlertsDetailsTable = memo(
),
},
{
field: 'ruleName',
field: KIBANA_ALERTS.RULE_NAME,
render: (ruleName: string) => <EuiText size="s">{ruleName}</EuiText>,
name: i18n.translate(
'xpack.securitySolution.flyout.left.insights.alerts.table.ruleNameColumnName',
@ -205,9 +275,10 @@ export const AlertsDetailsTable = memo(
}
),
width: '55%',
sortable: true,
},
{
field: 'severity',
field: KIBANA_ALERTS.SEVERITY,
render: (severity: AlertSeverity) => (
<EuiText size="s">
<SeverityBadge value={severity} data-test-subj="severityPropertyValue" />
@ -220,9 +291,10 @@ export const AlertsDetailsTable = memo(
}
),
width: '20%',
sortable: true,
},
{
field: 'status',
field: KIBANA_ALERTS.WORKFLOW_STATUS,
render: (status: string) => <EuiText size="s">{capitalize(status)}</EuiText>,
name: i18n.translate(
'xpack.securitySolution.flyout.left.insights.alerts.table.statusColumnName',
@ -231,6 +303,7 @@ export const AlertsDetailsTable = memo(
}
),
width: '20%',
sortable: true,
},
];
@ -281,6 +354,7 @@ export const AlertsDetailsTable = memo(
pagination={pagination}
onChange={onTableChange}
data-test-subj={'securitySolutionFlyoutMisconfigurationFindingsTable'}
sorting={sorting}
/>
</EuiPanel>
</>

View file

@ -5,15 +5,18 @@
* 2.0.
*/
import React, { memo, useEffect, useState } from 'react';
import type { Criteria, EuiBasicTableColumn } from '@elastic/eui';
import React, { memo, useCallback, useEffect, useState } from 'react';
import type { Criteria, EuiBasicTableColumn, EuiTableSortingType } 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 type { MisconfigurationFindingDetailFields } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_findings';
import {
useMisconfigurationFindings,
MISCONFIGURATION,
} from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_findings';
import { i18n } from '@kbn/i18n';
import type { CspFindingResult } 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';
@ -32,7 +35,11 @@ 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'>;
type MisconfigurationSortFieldType =
| MISCONFIGURATION.RESULT_EVALUATION
| MISCONFIGURATION.RULE_NAME
| 'resource'
| 'rule';
const getFindingsStats = (
passedFindingsStats: number,
@ -95,9 +102,17 @@ export const MisconfigurationFindingsDetailsTable = memo(
const [currentFilter, setCurrentFilter] = useState<string>('');
const [sortField, setSortField] = useState<MisconfigurationSortFieldType>(
MISCONFIGURATION.RESULT_EVALUATION
);
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const sortFieldDirection: { [key: string]: string } = {};
sortFieldDirection[sortField] = sortDirection;
const { data } = useMisconfigurationFindings({
query: buildMisconfigurationEntityFlyoutPreviewQuery(field, value, currentFilter),
sort: [],
sort: [sortFieldDirection],
enabled: true,
pageSize: 1,
});
@ -107,6 +122,13 @@ export const MisconfigurationFindingsDetailsTable = memo(
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const sorting: EuiTableSortingType<MisconfigurationFindingDetailFields> = {
sort: {
field: sortField,
direction: sortDirection,
},
};
const findingsPagination = (findings: MisconfigurationFindingDetailFields[]) => {
let pageOfItems;
@ -134,14 +156,21 @@ export const MisconfigurationFindingsDetailsTable = memo(
totalItemCount,
pageSizeOptions: [10, 25, 100],
};
const onTableChange = ({ page }: Criteria<MisconfigurationFindingDetailFields>) => {
if (page) {
const { index, size } = page;
setPageIndex(index);
setPageSize(size);
}
};
const onTableChange = useCallback(
({ page, sort }: Criteria<MisconfigurationFindingDetailFields>) => {
if (page) {
const { index, size } = page;
setPageIndex(index);
setPageSize(size);
}
if (sort) {
const { field: fieldSort, direction } = sort;
setSortField(fieldSort);
setSortDirection(direction);
}
},
[]
);
const getNavUrlParams = useGetNavigationUrlParams();
@ -189,8 +218,10 @@ export const MisconfigurationFindingsDetailsTable = memo(
),
},
{
field: 'result',
render: (result: CspFindingResult) => <CspEvaluationBadge type={result?.evaluation} />,
field: MISCONFIGURATION.RESULT_EVALUATION,
render: (result: CspFindingResult['evaluation'] | undefined) => (
<CspEvaluationBadge type={result} />
),
name: i18n.translate(
'xpack.securitySolution.flyout.left.insights.misconfigurations.table.resultColumnName',
{
@ -198,10 +229,13 @@ export const MisconfigurationFindingsDetailsTable = memo(
}
),
width: `${resultWidth}px`,
sortable: true,
},
{
field: 'rule',
render: (rule: CspBenchmarkRuleMetadata) => <EuiText size="s">{rule?.name}</EuiText>,
field: MISCONFIGURATION.RULE_NAME,
render: (ruleName: CspBenchmarkRuleMetadata['name']) => (
<EuiText size="s">{ruleName}</EuiText>
),
name: i18n.translate(
'xpack.securitySolution.flyout.left.insights.misconfigurations.table.ruleColumnName',
{
@ -209,6 +243,7 @@ export const MisconfigurationFindingsDetailsTable = memo(
}
),
width: `calc(100% - ${linkWidth + resultWidth}px)`,
sortable: true,
},
];
@ -245,6 +280,7 @@ export const MisconfigurationFindingsDetailsTable = memo(
pagination={pagination}
onChange={onTableChange}
data-test-subj={'securitySolutionFlyoutMisconfigurationFindingsTable'}
sorting={sorting}
/>
</EuiPanel>
</>

View file

@ -6,7 +6,7 @@
*/
import React, { memo, useEffect, useState } from 'react';
import type { Criteria, EuiBasicTableColumn } from '@elastic/eui';
import type { Criteria, EuiBasicTableColumn, EuiTableSortingType } from '@elastic/eui';
import { EuiSpacer, EuiPanel, EuiText, EuiBasicTable, EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
@ -14,11 +14,14 @@ import {
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 {
CspVulnerabilityFinding,
Vulnerability,
} from '@kbn/cloud-security-posture-common/schema/vulnerabilities/csp_vulnerability_finding';
VulnerabilitiesFindingDetailFields,
VulnerabilitiesPackage,
} from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_findings';
import {
useVulnerabilitiesFindings,
VULNERABILITY,
} from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_findings';
import {
getVulnerabilityStats,
CVSScoreBadge,
@ -35,17 +38,13 @@ import { useGetNavigationUrlParams } from '@kbn/cloud-security-posture/src/hooks
import { useHasVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_has_vulnerabilities';
import { SecuritySolutionLinkAnchor } from '../../../common/components/links';
type VulnerabilitiesFindingDetailFields = Pick<
CspVulnerabilityFinding,
'vulnerability' | 'resource'
>;
interface VulnerabilitiesPackage extends Vulnerability {
package: {
name: string;
version: string;
};
}
type VulnerabilitySortFieldType =
| 'score'
| 'vulnerability'
| 'resource'
| VULNERABILITY.SEVERITY
| VULNERABILITY.ID
| VULNERABILITY.PACKAGE_NAME;
export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: string }) => {
useEffect(() => {
@ -56,10 +55,23 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
}, []);
const [currentFilter, setCurrentFilter] = useState<string>('');
const [sortField, setSortField] = useState<VulnerabilitySortFieldType>(VULNERABILITY.SEVERITY);
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const sortFieldDirection: { [key: string]: string } = {};
sortFieldDirection[sortField === 'score' ? 'vulnerability.score.base' : sortField] =
sortDirection;
const sorting: EuiTableSortingType<VulnerabilitiesFindingDetailFields> = {
sort: {
field: sortField,
direction: sortDirection,
},
};
const { data } = useVulnerabilitiesFindings({
query: buildVulnerabilityEntityFlyoutPreviewQuery('host.name', value, currentFilter),
sort: [],
sort: [sortFieldDirection],
enabled: true,
pageSize: 1,
});
@ -96,12 +108,17 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
pageSizeOptions: [10, 25, 100],
};
const onTableChange = ({ page }: Criteria<VulnerabilitiesFindingDetailFields>) => {
const onTableChange = ({ page, sort }: Criteria<VulnerabilitiesFindingDetailFields>) => {
if (page) {
const { index, size } = page;
setPageIndex(index);
setPageSize(size);
}
if (sort) {
const { field: fieldSort, direction } = sort;
setSortField(fieldSort);
setSortDirection(direction);
}
};
const getNavUrlParams = useGetNavigationUrlParams();
@ -164,22 +181,20 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
),
},
{
field: 'vulnerability',
render: (vulnerability: Vulnerability) => <EuiText size="s">{vulnerability?.id}</EuiText>,
field: VULNERABILITY.ID,
render: (id: string) => <EuiText size="s">{id}</EuiText>,
name: i18n.translate(
'xpack.securitySolution.flyout.left.insights.vulnerability.table.resultColumnName',
{ defaultMessage: 'Vulnerability' }
),
width: '20%',
sortable: true,
},
{
field: 'vulnerability',
render: (vulnerability: Vulnerability) => (
field: 'score',
render: (score: { version?: string; base?: number }) => (
<EuiText size="s">
<CVSScoreBadge
version={vulnerability?.score?.version}
score={vulnerability?.score?.base}
/>
<CVSScoreBadge version={score?.version} score={score?.base} />
</EuiText>
),
name: i18n.translate(
@ -187,15 +202,14 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
{ defaultMessage: 'CVSS' }
),
width: '15%',
sortable: true,
},
{
field: 'vulnerability',
render: (vulnerability: Vulnerability) => (
field: VULNERABILITY.SEVERITY,
render: (severity: string) => (
<>
<EuiText size="s">
<SeverityStatusBadge
severity={vulnerability?.severity?.toUpperCase() as VulnSeverity}
/>
<SeverityStatusBadge severity={severity?.toUpperCase() as VulnSeverity} />
</EuiText>
</>
),
@ -204,17 +218,17 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
{ defaultMessage: 'Severity' }
),
width: '20%',
sortable: true,
},
{
field: 'vulnerability',
render: (vulnerability: VulnerabilitiesPackage) => (
<EuiText size="s">{vulnerability?.package?.name}</EuiText>
),
field: VULNERABILITY.PACKAGE_NAME,
render: (packageName: string) => <EuiText size="s">{packageName}</EuiText>,
name: i18n.translate(
'xpack.securitySolution.flyout.left.insights.vulnerability.table.ruleColumnName',
{ defaultMessage: 'Package' }
),
width: '40%',
sortable: true,
},
];
@ -248,6 +262,7 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
pagination={pagination}
onChange={onTableChange}
data-test-subj={'securitySolutionFlyoutVulnerabilitiesFindingsTable'}
sorting={sorting}
/>
</EuiPanel>
</>