mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Cloud Security] enable leading wildcard search and improve error handling when it is disabled (#162555)
## Summary
The PR:
- fixes https://github.com/elastic/kibana/issues/157954
- allows a user to recover from
https://github.com/elastic/kibana/issues/157954 by rendering the search
bar even when an error occurs (in line with how Misconfiguration
behaves)
- enables the support of `query:allowLeadingWildcard` setting of Kibana
- fixes unhandled exception on Resource Vulnerabilities page
- fixes the loading state on the search bar in Vulnerabilities which
wasn't working before

---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
5b283efe32
commit
c237e11a60
6 changed files with 448 additions and 353 deletions
|
@ -6,17 +6,26 @@
|
|||
*/
|
||||
|
||||
import { useEffect, useCallback, useMemo } from 'react';
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
import { buildEsQuery, EsQueryConfig } from '@kbn/es-query';
|
||||
import type { EuiBasicTableProps, Pagination } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { type Query } from '@kbn/es-query';
|
||||
import { useKibana } from '../use_kibana';
|
||||
import type { FindingsBaseProps, FindingsBaseURLQuery } from '../../types';
|
||||
import type {
|
||||
FindingsBaseESQueryConfig,
|
||||
FindingsBaseProps,
|
||||
FindingsBaseURLQuery,
|
||||
} from '../../types';
|
||||
|
||||
const getBaseQuery = ({ dataView, query, filters }: FindingsBaseURLQuery & FindingsBaseProps) => {
|
||||
const getBaseQuery = ({
|
||||
dataView,
|
||||
query,
|
||||
filters,
|
||||
config,
|
||||
}: FindingsBaseURLQuery & FindingsBaseProps & FindingsBaseESQueryConfig) => {
|
||||
try {
|
||||
return {
|
||||
query: buildEsQuery(dataView, query, filters), // will throw for malformed query
|
||||
query: buildEsQuery(dataView, query, filters, config), // will throw for malformed query
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
|
@ -56,11 +65,13 @@ export const useBaseEsQuery = ({
|
|||
data: {
|
||||
query: { filterManager, queryString },
|
||||
},
|
||||
uiSettings,
|
||||
} = useKibana().services;
|
||||
|
||||
const allowLeadingWildcards = uiSettings.get('query:allowLeadingWildcards');
|
||||
const config: EsQueryConfig = useMemo(() => ({ allowLeadingWildcards }), [allowLeadingWildcards]);
|
||||
const baseEsQuery = useMemo(
|
||||
() => getBaseQuery({ dataView, filters, query }),
|
||||
[dataView, filters, query]
|
||||
() => getBaseQuery({ dataView, filters, query, config }),
|
||||
[dataView, filters, query, config]
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import type { Criteria } from '@elastic/eui';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { BoolQuery, Filter, Query } from '@kbn/es-query';
|
||||
import type { BoolQuery, Filter, Query, EsQueryConfig } from '@kbn/es-query';
|
||||
import { CspFinding } from '../../common/schemas/csp_finding';
|
||||
|
||||
export type FindingsGroupByKind = 'default' | 'resource';
|
||||
|
@ -20,6 +20,10 @@ export interface FindingsBaseProps {
|
|||
dataView: DataView;
|
||||
}
|
||||
|
||||
export interface FindingsBaseESQueryConfig {
|
||||
config: EsQueryConfig;
|
||||
}
|
||||
|
||||
export interface FindingsBaseEsQuery {
|
||||
query?: {
|
||||
bool: BoolQuery;
|
||||
|
|
|
@ -131,3 +131,29 @@ export interface Vector {
|
|||
vector: string;
|
||||
score: number | undefined;
|
||||
}
|
||||
|
||||
export interface VulnerabilitiesQueryData {
|
||||
page: VulnerabilityRecord[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface VulnerabilitiesByResourceQueryData {
|
||||
page: Array<{
|
||||
resource: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
cloud: {
|
||||
region: string;
|
||||
};
|
||||
vulnerabilities_count: number;
|
||||
severity_map: {
|
||||
critical: number;
|
||||
high: number;
|
||||
medium: number;
|
||||
low: number;
|
||||
};
|
||||
}>;
|
||||
total: number;
|
||||
total_vulnerabilities: number;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import { Routes, Route } from '@kbn/shared-ux-router';
|
|||
import { LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY } from '../../common/constants';
|
||||
import { useCloudPostureTable } from '../../common/hooks/use_cloud_posture_table';
|
||||
import { useLatestVulnerabilities } from './hooks/use_latest_vulnerabilities';
|
||||
import { VulnerabilityRecord } from './types';
|
||||
import type { VulnerabilityRecord, VulnerabilitiesQueryData } from './types';
|
||||
import { LATEST_VULNERABILITIES_INDEX_PATTERN } from '../../../common/constants';
|
||||
import { ErrorCallout } from '../configurations/layout/error_callout';
|
||||
import { FindingsSearchBar } from '../configurations/layout/findings_search_bar';
|
||||
|
@ -100,12 +100,18 @@ export const Vulnerabilities = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => {
|
||||
const VulnerabilitiesDataGrid = ({
|
||||
dataView,
|
||||
data,
|
||||
isFetching,
|
||||
}: {
|
||||
dataView: DataView;
|
||||
data: VulnerabilitiesQueryData | undefined;
|
||||
isFetching: boolean;
|
||||
}) => {
|
||||
const {
|
||||
pageIndex,
|
||||
query,
|
||||
sort,
|
||||
queryError,
|
||||
pageSize,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
|
@ -120,9 +126,20 @@ const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => {
|
|||
});
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const styles = useStyles();
|
||||
|
||||
const [showHighlight, setHighlight] = useState(false);
|
||||
|
||||
const invalidIndex = -1;
|
||||
|
||||
const selectedVulnerability = useMemo(() => {
|
||||
return data?.page[urlQuery.vulnerabilityIndex];
|
||||
}, [data?.page, urlQuery.vulnerabilityIndex]);
|
||||
|
||||
const onCloseFlyout = () => {
|
||||
setUrlQuery({
|
||||
vulnerabilityIndex: invalidIndex,
|
||||
});
|
||||
};
|
||||
|
||||
const onSortHandler = useCallback(
|
||||
(newSort: any) => {
|
||||
onSort(newSort);
|
||||
|
@ -136,41 +153,12 @@ const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => {
|
|||
[onSort, sort]
|
||||
);
|
||||
|
||||
const multiFieldsSort = useMemo(() => {
|
||||
return sort.map(({ id, direction }: { id: string; direction: string }) => {
|
||||
if (id === vulnerabilitiesColumns.severity) {
|
||||
return severitySortScript(direction);
|
||||
}
|
||||
if (id === vulnerabilitiesColumns.package) {
|
||||
return getCaseInsensitiveSortScript(id, direction);
|
||||
}
|
||||
|
||||
return {
|
||||
[id]: direction,
|
||||
};
|
||||
});
|
||||
}, [sort]);
|
||||
|
||||
const { data, isLoading, isFetching } = useLatestVulnerabilities({
|
||||
query,
|
||||
sort: multiFieldsSort,
|
||||
enabled: !queryError,
|
||||
const { isLastLimitedPage, limitedTotalItemCount } = useLimitProperties({
|
||||
total: data?.total,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
const invalidIndex = -1;
|
||||
|
||||
const selectedVulnerability = useMemo(() => {
|
||||
return data?.page[urlQuery.vulnerabilityIndex];
|
||||
}, [data?.page, urlQuery.vulnerabilityIndex]);
|
||||
|
||||
const onCloseFlyout = () => {
|
||||
setUrlQuery({
|
||||
vulnerabilityIndex: invalidIndex,
|
||||
});
|
||||
};
|
||||
|
||||
const onOpenFlyout = useCallback(
|
||||
(vulnerabilityRow: VulnerabilityRecord) => {
|
||||
const vulnerabilityIndex = data?.page.findIndex(
|
||||
|
@ -189,12 +177,6 @@ const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => {
|
|||
[setUrlQuery, data?.page]
|
||||
);
|
||||
|
||||
const { isLastLimitedPage, limitedTotalItemCount } = useLimitProperties({
|
||||
total: data?.total,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
const columns = useMemo(() => {
|
||||
if (!data?.page) {
|
||||
return [];
|
||||
|
@ -321,21 +303,132 @@ const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => {
|
|||
[pageSize, setUrlQuery]
|
||||
);
|
||||
|
||||
const showVulnerabilityFlyout = flyoutVulnerabilityIndex > invalidIndex;
|
||||
|
||||
if (data?.page.length === 0) {
|
||||
return <EmptyState onResetFilters={onResetFilters} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiProgress
|
||||
size="xs"
|
||||
color="accent"
|
||||
style={{
|
||||
opacity: isFetching ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
<EuiDataGrid
|
||||
className={cx({ [styles.gridStyle]: true }, { [styles.highlightStyle]: showHighlight })}
|
||||
aria-label={VULNERABILITIES}
|
||||
columns={columns}
|
||||
columnVisibility={{
|
||||
visibleColumns: columns.map(({ id }) => id),
|
||||
setVisibleColumns: () => {},
|
||||
}}
|
||||
schemaDetectors={[severitySchemaConfig]}
|
||||
rowCount={limitedTotalItemCount}
|
||||
toolbarVisibility={{
|
||||
showColumnSelector: false,
|
||||
showDisplaySelector: false,
|
||||
showKeyboardShortcuts: false,
|
||||
showFullScreenSelector: false,
|
||||
additionalControls: {
|
||||
left: {
|
||||
prepend: (
|
||||
<>
|
||||
<EuiButtonEmpty size="xs" color="text">
|
||||
{i18n.translate('xpack.csp.vulnerabilities.totalVulnerabilities', {
|
||||
defaultMessage:
|
||||
'{total, plural, one {# Vulnerability} other {# Vulnerabilities}}',
|
||||
values: { total: data?.total },
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
),
|
||||
},
|
||||
right: (
|
||||
<EuiFlexItem grow={false} className={styles.groupBySelector}>
|
||||
<FindingsGroupBySelector
|
||||
type="default"
|
||||
pathnameHandler={vulnerabilitiesPathnameHandler}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
),
|
||||
},
|
||||
}}
|
||||
gridStyle={{
|
||||
border: 'horizontal',
|
||||
cellPadding: 'l',
|
||||
stripes: false,
|
||||
rowHover: 'none',
|
||||
header: 'underline',
|
||||
}}
|
||||
renderCellValue={renderCellValue}
|
||||
inMemory={{ level: 'enhancements' }}
|
||||
sorting={{ columns: sort, onSort: onSortHandler }}
|
||||
pagination={{
|
||||
pageIndex,
|
||||
pageSize,
|
||||
pageSizeOptions: [10, 25, 100],
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}}
|
||||
/>
|
||||
{isLastLimitedPage && <LimitedResultsBar />}
|
||||
{showVulnerabilityFlyout && selectedVulnerability && (
|
||||
<VulnerabilityFindingFlyout
|
||||
flyoutIndex={selectedVulnerabilityIndex}
|
||||
vulnerabilityRecord={selectedVulnerability}
|
||||
totalVulnerabilitiesCount={limitedTotalItemCount}
|
||||
onPaginate={onPaginateFlyout}
|
||||
closeFlyout={onCloseFlyout}
|
||||
isLoading={isFetching}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => {
|
||||
const { pageIndex, query, sort, queryError, pageSize, setUrlQuery } = useCloudPostureTable({
|
||||
dataView,
|
||||
defaultQuery: getDefaultQuery,
|
||||
paginationLocalStorageKey: LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY,
|
||||
});
|
||||
|
||||
const multiFieldsSort = useMemo(() => {
|
||||
return sort.map(({ id, direction }: { id: string; direction: string }) => {
|
||||
if (id === vulnerabilitiesColumns.severity) {
|
||||
return severitySortScript(direction);
|
||||
}
|
||||
if (id === vulnerabilitiesColumns.package) {
|
||||
return getCaseInsensitiveSortScript(id, direction);
|
||||
}
|
||||
|
||||
return {
|
||||
[id]: direction,
|
||||
};
|
||||
});
|
||||
}, [sort]);
|
||||
|
||||
const { data, isLoading, isFetching } = useLatestVulnerabilities({
|
||||
query,
|
||||
sort: multiFieldsSort,
|
||||
enabled: !queryError,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
const error = queryError || null;
|
||||
|
||||
if (error) {
|
||||
return <ErrorCallout error={error as Error} />;
|
||||
}
|
||||
if (isLoading) {
|
||||
if (isLoading && !error) {
|
||||
return defaultLoadingRenderer();
|
||||
}
|
||||
|
||||
if (!data?.page) {
|
||||
if (!data?.page && !error) {
|
||||
return defaultNoDataRenderer();
|
||||
}
|
||||
|
||||
const showVulnerabilityFlyout = flyoutVulnerabilityIndex > invalidIndex;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FindingsSearchBar
|
||||
|
@ -343,90 +436,13 @@ const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => {
|
|||
setQuery={(newQuery) => {
|
||||
setUrlQuery({ ...newQuery, pageIndex: 0 });
|
||||
}}
|
||||
loading={isLoading}
|
||||
loading={isFetching}
|
||||
placeholder={SEARCH_BAR_PLACEHOLDER}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
{!isLoading && data.page.length === 0 ? (
|
||||
<EmptyState onResetFilters={onResetFilters} />
|
||||
) : (
|
||||
<>
|
||||
<EuiProgress
|
||||
size="xs"
|
||||
color="accent"
|
||||
style={{
|
||||
opacity: isFetching ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
<EuiDataGrid
|
||||
className={cx({ [styles.gridStyle]: true }, { [styles.highlightStyle]: showHighlight })}
|
||||
aria-label={VULNERABILITIES}
|
||||
columns={columns}
|
||||
columnVisibility={{
|
||||
visibleColumns: columns.map(({ id }) => id),
|
||||
setVisibleColumns: () => {},
|
||||
}}
|
||||
schemaDetectors={[severitySchemaConfig]}
|
||||
rowCount={limitedTotalItemCount}
|
||||
toolbarVisibility={{
|
||||
showColumnSelector: false,
|
||||
showDisplaySelector: false,
|
||||
showKeyboardShortcuts: false,
|
||||
showFullScreenSelector: false,
|
||||
additionalControls: {
|
||||
left: {
|
||||
prepend: (
|
||||
<>
|
||||
<EuiButtonEmpty size="xs" color="text">
|
||||
{i18n.translate('xpack.csp.vulnerabilities.totalVulnerabilities', {
|
||||
defaultMessage:
|
||||
'{total, plural, one {# Vulnerability} other {# Vulnerabilities}}',
|
||||
values: { total: data?.total },
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
),
|
||||
},
|
||||
right: (
|
||||
<EuiFlexItem grow={false} className={styles.groupBySelector}>
|
||||
<FindingsGroupBySelector
|
||||
type="default"
|
||||
pathnameHandler={vulnerabilitiesPathnameHandler}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
),
|
||||
},
|
||||
}}
|
||||
gridStyle={{
|
||||
border: 'horizontal',
|
||||
cellPadding: 'l',
|
||||
stripes: false,
|
||||
rowHover: 'none',
|
||||
header: 'underline',
|
||||
}}
|
||||
renderCellValue={renderCellValue}
|
||||
inMemory={{ level: 'enhancements' }}
|
||||
sorting={{ columns: sort, onSort: onSortHandler }}
|
||||
pagination={{
|
||||
pageIndex,
|
||||
pageSize,
|
||||
pageSizeOptions: [10, 25, 100],
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}}
|
||||
/>
|
||||
{isLastLimitedPage && <LimitedResultsBar />}
|
||||
{showVulnerabilityFlyout && selectedVulnerability && (
|
||||
<VulnerabilityFindingFlyout
|
||||
flyoutIndex={selectedVulnerabilityIndex}
|
||||
vulnerabilityRecord={selectedVulnerability}
|
||||
totalVulnerabilitiesCount={limitedTotalItemCount}
|
||||
onPaginate={onPaginateFlyout}
|
||||
closeFlyout={onCloseFlyout}
|
||||
isLoading={isFetching}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{error && <ErrorCallout error={error as Error} />}
|
||||
{!error && (
|
||||
<VulnerabilitiesDataGrid dataView={dataView} data={data} isFetching={isFetching} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -19,10 +19,11 @@ import React, { useCallback, useMemo, useState, useEffect } from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { Link, useParams, generatePath } from 'react-router-dom';
|
||||
import type { BoolQuery } from '@kbn/es-query';
|
||||
import { LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY } from '../../../../common/constants';
|
||||
import { useCloudPostureTable } from '../../../../common/hooks/use_cloud_posture_table';
|
||||
import { useLatestVulnerabilities } from '../../hooks/use_latest_vulnerabilities';
|
||||
import { VulnerabilityRecord } from '../../types';
|
||||
import type { VulnerabilityRecord, VulnerabilitiesQueryData } from '../../types';
|
||||
import { ErrorCallout } from '../../../configurations/layout/error_callout';
|
||||
import { FindingsSearchBar } from '../../../configurations/layout/findings_search_bar';
|
||||
import { CVSScoreBadge, SeverityStatusBadge } from '../../../../components/vulnerability_badges';
|
||||
|
@ -63,15 +64,18 @@ const getDefaultQuery = ({ query, filters }: any) => ({
|
|||
pageIndex: 0,
|
||||
});
|
||||
|
||||
export const ResourceVulnerabilities = ({ dataView }: { dataView: DataView }) => {
|
||||
const params = useParams<{ resourceId: string }>();
|
||||
const resourceId = decodeURIComponent(params.resourceId);
|
||||
|
||||
const ResourceVulnerabilitiesDataGrid = ({
|
||||
dataView,
|
||||
data,
|
||||
isFetching,
|
||||
}: {
|
||||
dataView: DataView;
|
||||
data: VulnerabilitiesQueryData;
|
||||
isFetching: boolean;
|
||||
}) => {
|
||||
const {
|
||||
pageIndex,
|
||||
query,
|
||||
sort,
|
||||
queryError,
|
||||
pageSize,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
|
@ -102,35 +106,6 @@ export const ResourceVulnerabilities = ({ dataView }: { dataView: DataView }) =>
|
|||
[onSort, sort]
|
||||
);
|
||||
|
||||
const multiFieldsSort = useMemo(() => {
|
||||
return sort.map(({ id, direction }: { id: string; direction: string }) => {
|
||||
if (id === vulnerabilitiesColumns.severity) {
|
||||
return severitySortScript(direction);
|
||||
}
|
||||
if (id === vulnerabilitiesColumns.package) {
|
||||
return getCaseInsensitiveSortScript(id, direction);
|
||||
}
|
||||
|
||||
return {
|
||||
[id]: direction,
|
||||
};
|
||||
});
|
||||
}, [sort]);
|
||||
|
||||
const { data, isLoading, isFetching } = useLatestVulnerabilities({
|
||||
query: {
|
||||
...query,
|
||||
bool: {
|
||||
...query!.bool,
|
||||
filter: [...(query?.bool?.filter || []), { term: { 'resource.id': resourceId } }],
|
||||
},
|
||||
},
|
||||
sort: multiFieldsSort,
|
||||
enabled: !queryError,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
const invalidIndex = -1;
|
||||
|
||||
const selectedVulnerability = useMemo(() => {
|
||||
|
@ -293,11 +268,130 @@ export const ResourceVulnerabilities = ({ dataView }: { dataView: DataView }) =>
|
|||
[pageSize, setUrlQuery]
|
||||
);
|
||||
|
||||
const showVulnerabilityFlyout = flyoutVulnerabilityIndex > invalidIndex;
|
||||
|
||||
if (data.page.length === 0) {
|
||||
return <EmptyState onResetFilters={onResetFilters} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiProgress
|
||||
size="xs"
|
||||
color="accent"
|
||||
style={{
|
||||
opacity: isFetching ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
<EuiDataGrid
|
||||
className={cx({ [styles.gridStyle]: true }, { [styles.highlightStyle]: showHighlight })}
|
||||
aria-label={VULNERABILITIES}
|
||||
columns={columns}
|
||||
columnVisibility={{
|
||||
visibleColumns: columns.map(({ id }) => id),
|
||||
setVisibleColumns: () => {},
|
||||
}}
|
||||
height={undefined}
|
||||
width={undefined}
|
||||
schemaDetectors={[severitySchemaConfig]}
|
||||
rowCount={limitedTotalItemCount}
|
||||
rowHeightsOptions={{
|
||||
defaultHeight: 40,
|
||||
}}
|
||||
toolbarVisibility={{
|
||||
showColumnSelector: false,
|
||||
showDisplaySelector: false,
|
||||
showKeyboardShortcuts: false,
|
||||
showFullScreenSelector: false,
|
||||
additionalControls: {
|
||||
left: {
|
||||
prepend: (
|
||||
<>
|
||||
<EuiButtonEmpty size="xs" color="text">
|
||||
{i18n.translate('xpack.csp.vulnerabilities.totalVulnerabilities', {
|
||||
defaultMessage:
|
||||
'{total, plural, one {# Vulnerability} other {# Vulnerabilities}}',
|
||||
values: { total: data?.total },
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
}}
|
||||
gridStyle={{
|
||||
border: 'horizontal',
|
||||
cellPadding: 'l',
|
||||
stripes: false,
|
||||
rowHover: 'none',
|
||||
header: 'underline',
|
||||
}}
|
||||
renderCellValue={renderCellValue}
|
||||
inMemory={{ level: 'enhancements' }}
|
||||
sorting={{ columns: sort, onSort: onSortHandler }}
|
||||
pagination={{
|
||||
pageIndex,
|
||||
pageSize,
|
||||
pageSizeOptions: [10, 25, 100],
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}}
|
||||
/>
|
||||
{isLastLimitedPage && <LimitedResultsBar />}
|
||||
{showVulnerabilityFlyout && selectedVulnerability && (
|
||||
<VulnerabilityFindingFlyout
|
||||
flyoutIndex={selectedVulnerabilityIndex}
|
||||
vulnerabilityRecord={selectedVulnerability}
|
||||
totalVulnerabilitiesCount={limitedTotalItemCount}
|
||||
onPaginate={onPaginateFlyout}
|
||||
closeFlyout={onCloseFlyout}
|
||||
isLoading={isFetching}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const ResourceVulnerabilities = ({ dataView }: { dataView: DataView }) => {
|
||||
const params = useParams<{ resourceId: string }>();
|
||||
const resourceId = decodeURIComponent(params.resourceId);
|
||||
|
||||
const { pageIndex, query, sort, queryError, pageSize, setUrlQuery } = useCloudPostureTable({
|
||||
dataView,
|
||||
defaultQuery: getDefaultQuery,
|
||||
paginationLocalStorageKey: LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY,
|
||||
});
|
||||
|
||||
const multiFieldsSort = useMemo(() => {
|
||||
return sort.map(({ id, direction }: { id: string; direction: string }) => {
|
||||
if (id === vulnerabilitiesColumns.severity) {
|
||||
return severitySortScript(direction);
|
||||
}
|
||||
if (id === vulnerabilitiesColumns.package) {
|
||||
return getCaseInsensitiveSortScript(id, direction);
|
||||
}
|
||||
|
||||
return {
|
||||
[id]: direction,
|
||||
};
|
||||
});
|
||||
}, [sort]);
|
||||
|
||||
const { data, isLoading, isFetching } = useLatestVulnerabilities({
|
||||
query: {
|
||||
...query,
|
||||
bool: {
|
||||
...(query?.bool as BoolQuery),
|
||||
filter: [...(query?.bool?.filter || []), { term: { 'resource.id': resourceId } }],
|
||||
},
|
||||
},
|
||||
sort: multiFieldsSort,
|
||||
enabled: !queryError,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
const error = queryError || null;
|
||||
|
||||
if (error) {
|
||||
return <ErrorCallout error={error as Error} />;
|
||||
}
|
||||
if (isLoading) {
|
||||
return defaultLoadingRenderer();
|
||||
}
|
||||
|
@ -306,8 +400,6 @@ export const ResourceVulnerabilities = ({ dataView }: { dataView: DataView }) =>
|
|||
return defaultNoDataRenderer();
|
||||
}
|
||||
|
||||
const showVulnerabilityFlyout = flyoutVulnerabilityIndex > invalidIndex;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FindingsSearchBar
|
||||
|
@ -315,7 +407,7 @@ export const ResourceVulnerabilities = ({ dataView }: { dataView: DataView }) =>
|
|||
setQuery={(newQuery) => {
|
||||
setUrlQuery({ ...newQuery, pageIndex: 0 });
|
||||
}}
|
||||
loading={isLoading}
|
||||
loading={isFetching}
|
||||
placeholder={SEARCH_BAR_PLACEHOLDER}
|
||||
/>
|
||||
<Link to={generatePath(findingsNavigation.vulnerabilities_by_resource.path)}>
|
||||
|
@ -363,83 +455,9 @@ export const ResourceVulnerabilities = ({ dataView }: { dataView: DataView }) =>
|
|||
/>
|
||||
<EuiSpacer />
|
||||
<EuiSpacer size="m" />
|
||||
{!isLoading && data?.page.length === 0 ? (
|
||||
<EmptyState onResetFilters={onResetFilters} />
|
||||
) : (
|
||||
<>
|
||||
<EuiProgress
|
||||
size="xs"
|
||||
color="accent"
|
||||
style={{
|
||||
opacity: isFetching ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
<EuiDataGrid
|
||||
className={cx({ [styles.gridStyle]: true }, { [styles.highlightStyle]: showHighlight })}
|
||||
aria-label={VULNERABILITIES}
|
||||
columns={columns}
|
||||
columnVisibility={{
|
||||
visibleColumns: columns.map(({ id }) => id),
|
||||
setVisibleColumns: () => {},
|
||||
}}
|
||||
height={undefined}
|
||||
width={undefined}
|
||||
schemaDetectors={[severitySchemaConfig]}
|
||||
rowCount={limitedTotalItemCount}
|
||||
rowHeightsOptions={{
|
||||
defaultHeight: 40,
|
||||
}}
|
||||
toolbarVisibility={{
|
||||
showColumnSelector: false,
|
||||
showDisplaySelector: false,
|
||||
showKeyboardShortcuts: false,
|
||||
showFullScreenSelector: false,
|
||||
additionalControls: {
|
||||
left: {
|
||||
prepend: (
|
||||
<>
|
||||
<EuiButtonEmpty size="xs" color="text">
|
||||
{i18n.translate('xpack.csp.vulnerabilities.totalVulnerabilities', {
|
||||
defaultMessage:
|
||||
'{total, plural, one {# Vulnerability} other {# Vulnerabilities}}',
|
||||
values: { total: data?.total },
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
}}
|
||||
gridStyle={{
|
||||
border: 'horizontal',
|
||||
cellPadding: 'l',
|
||||
stripes: false,
|
||||
rowHover: 'none',
|
||||
header: 'underline',
|
||||
}}
|
||||
renderCellValue={renderCellValue}
|
||||
inMemory={{ level: 'enhancements' }}
|
||||
sorting={{ columns: sort, onSort: onSortHandler }}
|
||||
pagination={{
|
||||
pageIndex,
|
||||
pageSize,
|
||||
pageSizeOptions: [10, 25, 100],
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}}
|
||||
/>
|
||||
{isLastLimitedPage && <LimitedResultsBar />}
|
||||
{showVulnerabilityFlyout && selectedVulnerability && (
|
||||
<VulnerabilityFindingFlyout
|
||||
flyoutIndex={selectedVulnerabilityIndex}
|
||||
vulnerabilityRecord={selectedVulnerability}
|
||||
totalVulnerabilitiesCount={limitedTotalItemCount}
|
||||
onPaginate={onPaginateFlyout}
|
||||
closeFlyout={onCloseFlyout}
|
||||
isLoading={isFetching}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{error && <ErrorCallout error={error as Error} />}
|
||||
{!error && (
|
||||
<ResourceVulnerabilitiesDataGrid dataView={dataView} data={data} isFetching={isFetching} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -41,6 +41,7 @@ import { EmptyState } from '../../../components/empty_state';
|
|||
import { SeverityMap } from './severity_map';
|
||||
import { VULNERABILITY_RESOURCE_COUNT } from './test_subjects';
|
||||
import { getVulnerabilitiesGridCellActions } from '../utils/get_vulnerabilities_grid_cell_actions';
|
||||
import type { VulnerabilitiesByResourceQueryData } from '../types';
|
||||
|
||||
const getDefaultQuery = ({ query, filters }: any): any => ({
|
||||
query,
|
||||
|
@ -49,12 +50,18 @@ const getDefaultQuery = ({ query, filters }: any): any => ({
|
|||
pageIndex: 0,
|
||||
});
|
||||
|
||||
export const VulnerabilitiesByResource = ({ dataView }: { dataView: DataView }) => {
|
||||
const VulnerabilitiesByResourceDataGrid = ({
|
||||
dataView,
|
||||
data,
|
||||
isFetching,
|
||||
}: {
|
||||
dataView: DataView;
|
||||
data: VulnerabilitiesByResourceQueryData | undefined;
|
||||
isFetching: boolean;
|
||||
}) => {
|
||||
const {
|
||||
pageIndex,
|
||||
query,
|
||||
sort,
|
||||
queryError,
|
||||
pageSize,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
|
@ -67,15 +74,7 @@ export const VulnerabilitiesByResource = ({ dataView }: { dataView: DataView })
|
|||
defaultQuery: getDefaultQuery,
|
||||
paginationLocalStorageKey: LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY,
|
||||
});
|
||||
|
||||
const styles = useStyles();
|
||||
const { data, isLoading, isFetching } = useLatestVulnerabilitiesByResource({
|
||||
query,
|
||||
sortOrder: sort[0]?.direction,
|
||||
enabled: !queryError,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
const { isLastLimitedPage, limitedTotalItemCount } = useLimitProperties({
|
||||
total: data?.total,
|
||||
|
@ -151,16 +150,109 @@ export const VulnerabilitiesByResource = ({ dataView }: { dataView: DataView })
|
|||
return Cell;
|
||||
}, [data?.page, pageSize, isFetching]);
|
||||
|
||||
if (data?.page.length === 0) {
|
||||
return <EmptyState onResetFilters={onResetFilters} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiProgress
|
||||
size="xs"
|
||||
color="accent"
|
||||
style={{
|
||||
opacity: isFetching ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
<EuiDataGrid
|
||||
className={styles.gridStyle}
|
||||
aria-label={VULNERABILITIES}
|
||||
columns={columns}
|
||||
columnVisibility={{
|
||||
visibleColumns: columns.map(({ id }) => id),
|
||||
setVisibleColumns: () => {},
|
||||
}}
|
||||
rowCount={limitedTotalItemCount}
|
||||
toolbarVisibility={{
|
||||
showColumnSelector: false,
|
||||
showDisplaySelector: false,
|
||||
showKeyboardShortcuts: false,
|
||||
showSortSelector: false,
|
||||
showFullScreenSelector: false,
|
||||
additionalControls: {
|
||||
left: {
|
||||
prepend: (
|
||||
<>
|
||||
<EuiButtonEmpty size="xs" color="text">
|
||||
{i18n.translate('xpack.csp.vulnerabilitiesByResource.totalResources', {
|
||||
defaultMessage: '{total, plural, one {# Resource} other {# Resources}}',
|
||||
values: { total: data?.total },
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButtonEmpty size="xs" color="text">
|
||||
{i18n.translate('xpack.csp.vulnerabilitiesByResource.totalVulnerabilities', {
|
||||
defaultMessage:
|
||||
'{total, plural, one {# Vulnerability} other {# Vulnerabilities}}',
|
||||
values: { total: data?.total_vulnerabilities },
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
),
|
||||
},
|
||||
right: (
|
||||
<EuiFlexItem grow={false} className={styles.groupBySelector}>
|
||||
<FindingsGroupBySelector
|
||||
type="resource"
|
||||
pathnameHandler={vulnerabilitiesPathnameHandler}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
),
|
||||
},
|
||||
}}
|
||||
gridStyle={{
|
||||
border: 'horizontal',
|
||||
cellPadding: 'l',
|
||||
stripes: false,
|
||||
rowHover: 'none',
|
||||
header: 'underline',
|
||||
}}
|
||||
renderCellValue={renderCellValue}
|
||||
inMemory={{ level: 'enhancements' }}
|
||||
sorting={{ columns: sort, onSort }}
|
||||
pagination={{
|
||||
pageIndex,
|
||||
pageSize,
|
||||
pageSizeOptions: [10, 25, 100],
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}}
|
||||
/>
|
||||
{isLastLimitedPage && <LimitedResultsBar />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const VulnerabilitiesByResource = ({ dataView }: { dataView: DataView }) => {
|
||||
const { pageIndex, query, sort, queryError, pageSize, setUrlQuery } = useCloudPostureTable({
|
||||
dataView,
|
||||
defaultQuery: getDefaultQuery,
|
||||
paginationLocalStorageKey: LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY,
|
||||
});
|
||||
|
||||
const { data, isLoading, isFetching } = useLatestVulnerabilitiesByResource({
|
||||
query,
|
||||
sortOrder: sort[0]?.direction,
|
||||
enabled: !queryError,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
const error = queryError || null;
|
||||
|
||||
if (error) {
|
||||
return <ErrorCallout error={error as Error} />;
|
||||
}
|
||||
if (isLoading) {
|
||||
if (isLoading && !error) {
|
||||
return defaultLoadingRenderer();
|
||||
}
|
||||
|
||||
if (!data?.page) {
|
||||
if (!data?.page && !error) {
|
||||
return defaultNoDataRenderer();
|
||||
}
|
||||
|
||||
|
@ -171,89 +263,17 @@ export const VulnerabilitiesByResource = ({ dataView }: { dataView: DataView })
|
|||
setQuery={(newQuery) => {
|
||||
setUrlQuery({ ...newQuery, pageIndex: 0 });
|
||||
}}
|
||||
loading={isLoading}
|
||||
loading={isFetching}
|
||||
placeholder={SEARCH_BAR_PLACEHOLDER}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
{!isLoading && data.page.length === 0 ? (
|
||||
<EmptyState onResetFilters={onResetFilters} />
|
||||
) : (
|
||||
<>
|
||||
<EuiProgress
|
||||
size="xs"
|
||||
color="accent"
|
||||
style={{
|
||||
opacity: isFetching ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
<EuiDataGrid
|
||||
className={styles.gridStyle}
|
||||
aria-label={VULNERABILITIES}
|
||||
columns={columns}
|
||||
columnVisibility={{
|
||||
visibleColumns: columns.map(({ id }) => id),
|
||||
setVisibleColumns: () => {},
|
||||
}}
|
||||
rowCount={limitedTotalItemCount}
|
||||
toolbarVisibility={{
|
||||
showColumnSelector: false,
|
||||
showDisplaySelector: false,
|
||||
showKeyboardShortcuts: false,
|
||||
showSortSelector: false,
|
||||
showFullScreenSelector: false,
|
||||
additionalControls: {
|
||||
left: {
|
||||
prepend: (
|
||||
<>
|
||||
<EuiButtonEmpty size="xs" color="text">
|
||||
{i18n.translate('xpack.csp.vulnerabilitiesByResource.totalResources', {
|
||||
defaultMessage: '{total, plural, one {# Resource} other {# Resources}}',
|
||||
values: { total: data?.total },
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButtonEmpty size="xs" color="text">
|
||||
{i18n.translate(
|
||||
'xpack.csp.vulnerabilitiesByResource.totalVulnerabilities',
|
||||
{
|
||||
defaultMessage:
|
||||
'{total, plural, one {# Vulnerability} other {# Vulnerabilities}}',
|
||||
values: { total: data?.total_vulnerabilities },
|
||||
}
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
),
|
||||
},
|
||||
right: (
|
||||
<EuiFlexItem grow={false} className={styles.groupBySelector}>
|
||||
<FindingsGroupBySelector
|
||||
type="resource"
|
||||
pathnameHandler={vulnerabilitiesPathnameHandler}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
),
|
||||
},
|
||||
}}
|
||||
gridStyle={{
|
||||
border: 'horizontal',
|
||||
cellPadding: 'l',
|
||||
stripes: false,
|
||||
rowHover: 'none',
|
||||
header: 'underline',
|
||||
}}
|
||||
renderCellValue={renderCellValue}
|
||||
inMemory={{ level: 'enhancements' }}
|
||||
sorting={{ columns: sort, onSort }}
|
||||
pagination={{
|
||||
pageIndex,
|
||||
pageSize,
|
||||
pageSizeOptions: [10, 25, 100],
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}}
|
||||
/>
|
||||
{isLastLimitedPage && <LimitedResultsBar />}
|
||||
</>
|
||||
{error && <ErrorCallout error={error as Error} />}
|
||||
{!error && (
|
||||
<VulnerabilitiesByResourceDataGrid
|
||||
dataView={dataView}
|
||||
data={data}
|
||||
isFetching={isFetching}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue