mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
fe988fa552
commit
9933cc6595
9 changed files with 337 additions and 115 deletions
|
@ -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)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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[],
|
||||
};
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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[],
|
||||
};
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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']) => {
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue