diff --git a/x-pack/plugins/cloud_security_posture/common/schemas/csp_vulnerability_finding.ts b/x-pack/plugins/cloud_security_posture/common/schemas/csp_vulnerability_finding.ts index efe26dc6648ba4..2dbab1bab77b64 100644 --- a/x-pack/plugins/cloud_security_posture/common/schemas/csp_vulnerability_finding.ts +++ b/x-pack/plugins/cloud_security_posture/common/schemas/csp_vulnerability_finding.ts @@ -87,7 +87,7 @@ export interface Vulnerability { id: string; title: string; reference: string; - severity: VulnSeverity; + severity?: VulnSeverity; cvss: { nvd: VectorScoreBase; redhat?: VectorScoreBase; diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/use_latest_findings_data_view.ts b/x-pack/plugins/cloud_security_posture/public/common/api/use_latest_findings_data_view.ts index c8e98703cdbf0f..442330a888a50f 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/api/use_latest_findings_data_view.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/api/use_latest_findings_data_view.ts @@ -9,9 +9,15 @@ import { useQuery } from '@tanstack/react-query'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { DataView } from '@kbn/data-plugin/common'; import { i18n } from '@kbn/i18n'; -import { LATEST_FINDINGS_INDEX_PATTERN } from '../../../common/constants'; +import { + LATEST_FINDINGS_INDEX_PATTERN, + LATEST_VULNERABILITIES_INDEX_PATTERN, +} from '../../../common/constants'; import { CspClientPluginStartDeps } from '../../types'; +/** + * TODO: Remove this static labels once https://github.com/elastic/kibana/issues/172615 is resolved + */ const cloudSecurityFieldLabels: Record = { 'result.evaluation': i18n.translate( 'xpack.csp.findings.findingsTable.findingsTableColumn.resultColumnLabel', @@ -45,6 +51,30 @@ const cloudSecurityFieldLabels: Record = { 'xpack.csp.findings.findingsTable.findingsTableColumn.lastCheckedColumnLabel', { defaultMessage: 'Last Checked' } ), + 'vulnerability.id': i18n.translate( + 'xpack.csp.findings.findingsTable.findingsTableColumn.vulnerabilityIdColumnLabel', + { defaultMessage: 'Vulnerability' } + ), + 'vulnerability.score.base': i18n.translate( + 'xpack.csp.findings.findingsTable.findingsTableColumn.vulnerabilityScoreColumnLabel', + { defaultMessage: 'CVSS' } + ), + 'vulnerability.severity': i18n.translate( + 'xpack.csp.findings.findingsTable.findingsTableColumn.vulnerabilitySeverityColumnLabel', + { defaultMessage: 'Severity' } + ), + 'package.name': i18n.translate( + 'xpack.csp.findings.findingsTable.findingsTableColumn.packageNameColumnLabel', + { defaultMessage: 'Package' } + ), + 'package.version': i18n.translate( + 'xpack.csp.findings.findingsTable.findingsTableColumn.packageVersionColumnLabel', + { defaultMessage: 'Version' } + ), + 'package.fixed_version': i18n.translate( + 'xpack.csp.findings.findingsTable.findingsTableColumn.packageFixedVersionColumnLabel', + { defaultMessage: 'Fix Version' } + ), } as const; /** @@ -61,7 +91,13 @@ export const useLatestFindingsDataView = (dataView: string) => { throw new Error(`Data view not found [Name: {${dataView}}]`); } - if (dataView === LATEST_FINDINGS_INDEX_PATTERN) { + /** + * TODO: Remove this update logic once https://github.com/elastic/kibana/issues/172615 is resolved + */ + if ( + dataView === LATEST_FINDINGS_INDEX_PATTERN || + dataView === LATEST_VULNERABILITIES_INDEX_PATTERN + ) { let shouldUpdate = false; Object.entries(cloudSecurityFieldLabels).forEach(([field, label]) => { if ( diff --git a/x-pack/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/constants.ts index 833f941c95292b..bd266c98b80153 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts @@ -49,6 +49,9 @@ export const LOCAL_STORAGE_DASHBOARD_BENCHMARK_SORT_KEY = 'cloudPosture:complianceDashboard:benchmarkSort'; export const LOCAL_STORAGE_FINDINGS_LAST_SELECTED_TAB_KEY = 'cloudPosture:findings:lastSelectedTab'; +export const LOCAL_STORAGE_VULNERABILITIES_GROUPING_KEY = 'cspLatestVulnerabilitiesGrouping'; +export const LOCAL_STORAGE_FINDINGS_GROUPING_KEY = 'cspLatestFindingsGrouping'; + export const SESSION_STORAGE_FIELDS_MODAL_SHOW_SELECTED = 'cloudPosture:fieldsModal:showSelected'; export type CloudPostureIntegrations = Record< @@ -225,3 +228,5 @@ export const NO_FINDINGS_STATUS_REFRESH_INTERVAL_MS = 10000; export const DETECTION_ENGINE_RULES_KEY = 'detection_engine_rules'; export const DETECTION_ENGINE_ALERTS_KEY = 'detection_engine_alerts'; + +export const DEFAULT_GROUPING_TABLE_HEIGHT = 512; diff --git a/x-pack/plugins/cloud_security_posture/public/common/contexts/data_view_context.ts b/x-pack/plugins/cloud_security_posture/public/common/contexts/data_view_context.ts new file mode 100644 index 00000000000000..a14928e7133e39 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/contexts/data_view_context.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createContext, useContext } from 'react'; +import { DataView } from '@kbn/data-views-plugin/common'; + +interface DataViewContextValue { + dataView: DataView; + dataViewRefetch?: () => void; + dataViewIsRefetching?: boolean; +} + +export const DataViewContext = createContext(undefined); + +/** + * Retrieve context's properties + */ +export const useDataViewContext = (): DataViewContextValue => { + const contextValue = useContext(DataViewContext); + + if (!contextValue) { + throw new Error('useDataViewContext can only be used within DataViewContext provider'); + } + + return contextValue; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_base_es_query.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_base_es_query.ts index 4adffa100e48c9..9d5f5f2bf268d7 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_base_es_query.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_base_es_query.ts @@ -5,10 +5,12 @@ * 2.0. */ +import { DataView } from '@kbn/data-views-plugin/common'; import { buildEsQuery, EsQueryConfig } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { useEffect, useMemo } from 'react'; -import { FindingsBaseESQueryConfig, FindingsBaseProps, FindingsBaseURLQuery } from '../../types'; +import { useDataViewContext } from '../../contexts/data_view_context'; +import { FindingsBaseESQueryConfig, FindingsBaseURLQuery } from '../../types'; import { useKibana } from '../use_kibana'; const getBaseQuery = ({ @@ -16,7 +18,10 @@ const getBaseQuery = ({ query, filters, config, -}: FindingsBaseURLQuery & FindingsBaseProps & FindingsBaseESQueryConfig) => { +}: FindingsBaseURLQuery & + FindingsBaseESQueryConfig & { + dataView: DataView; + }) => { try { return { query: buildEsQuery(dataView, query, filters, config), // will throw for malformed query @@ -30,11 +35,10 @@ const getBaseQuery = ({ }; export const useBaseEsQuery = ({ - dataView, filters = [], query, nonPersistedFilters, -}: FindingsBaseURLQuery & FindingsBaseProps) => { +}: FindingsBaseURLQuery) => { const { notifications: { toasts }, data: { @@ -42,6 +46,7 @@ export const useBaseEsQuery = ({ }, uiSettings, } = useKibana().services; + const { dataView } = useDataViewContext(); const allowLeadingWildcards = uiSettings.get('query:allowLeadingWildcards'); const config: EsQueryConfig = useMemo(() => ({ allowLeadingWildcards }), [allowLeadingWildcards]); const baseEsQuery = useMemo( diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts index ae21f45c7a4e82..03517383ecc3fb 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts @@ -5,7 +5,6 @@ * 2.0. */ import { Dispatch, SetStateAction, useCallback } from 'react'; -import { type DataView } from '@kbn/data-views-plugin/common'; import { BoolQuery, Filter } from '@kbn/es-query'; import { CriteriaWithPagination } from '@elastic/eui'; import { DataTableRecord } from '@kbn/discover-utils/types'; @@ -46,13 +45,11 @@ export interface CloudPostureDataTableResult { */ export const useCloudPostureDataTable = ({ defaultQuery = getDefaultQuery, - dataView, paginationLocalStorageKey, columnsLocalStorageKey, nonPersistedFilters, }: { defaultQuery?: (params: FindingsBaseURLQuery) => FindingsBaseURLQuery; - dataView: DataView; paginationLocalStorageKey: string; columnsLocalStorageKey?: string; nonPersistedFilters?: Filter[]; @@ -116,7 +113,6 @@ export const useCloudPostureDataTable = ({ * Page URL query to ES query */ const baseEsQuery = useBaseEsQuery({ - dataView, filters: urlQuery.filters, query: urlQuery.query, ...(nonPersistedFilters ? { nonPersistedFilters } : {}), diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/index.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/index.ts deleted file mode 100644 index 06ad2776fb305b..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './use_cloud_posture_table'; diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts deleted file mode 100644 index d06e29a95e46d7..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { Dispatch, SetStateAction, useCallback } from 'react'; -import { type DataView } from '@kbn/data-views-plugin/common'; -import { BoolQuery } from '@kbn/es-query'; -import { CriteriaWithPagination } from '@elastic/eui'; -import { DataTableRecord } from '@kbn/discover-utils/types'; -import { useUrlQuery } from '../use_url_query'; -import { usePageSize } from '../use_page_size'; -import { getDefaultQuery, useBaseEsQuery, usePersistedQuery } from './utils'; -import { LOCAL_STORAGE_DATA_TABLE_COLUMNS_KEY } from '../../constants'; - -export interface CloudPostureTableResult { - // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable - setUrlQuery: (query: any) => void; - // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable - sort: any; - // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable - filters: any[]; - query?: { bool: BoolQuery }; - queryError?: Error; - pageIndex: number; - // TODO: remove any, urlQuery is an object with query fields but we also add custom fields to it, need to assert usages - urlQuery: any; - setTableOptions: (options: CriteriaWithPagination) => void; - // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable - handleUpdateQuery: (query: any) => void; - pageSize: number; - setPageSize: Dispatch>; - onChangeItemsPerPage: (newPageSize: number) => void; - onChangePage: (newPageIndex: number) => void; - // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable - onSort: (sort: any) => void; - onResetFilters: () => void; - columnsLocalStorageKey: string; - getRowsFromPages: (data: Array<{ page: DataTableRecord[] }> | undefined) => DataTableRecord[]; -} - -/** - * @deprecated will be replaced by useCloudPostureDataTable - */ -export const useCloudPostureTable = ({ - defaultQuery = getDefaultQuery, - dataView, - paginationLocalStorageKey, - columnsLocalStorageKey, -}: { - // TODO: Remove any when all finding tables are converted to CloudSecurityDataTable - defaultQuery?: (params: any) => any; - dataView: DataView; - paginationLocalStorageKey: string; - columnsLocalStorageKey?: string; -}): CloudPostureTableResult => { - const getPersistedDefaultQuery = usePersistedQuery(defaultQuery); - const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); - const { pageSize, setPageSize } = usePageSize(paginationLocalStorageKey); - - const onChangeItemsPerPage = useCallback( - (newPageSize) => { - setPageSize(newPageSize); - setUrlQuery({ - pageIndex: 0, - pageSize: newPageSize, - }); - }, - [setPageSize, setUrlQuery] - ); - - const onResetFilters = useCallback(() => { - setUrlQuery({ - pageIndex: 0, - filters: [], - query: { - query: '', - language: 'kuery', - }, - }); - }, [setUrlQuery]); - - const onChangePage = useCallback( - (newPageIndex) => { - setUrlQuery({ - pageIndex: newPageIndex, - }); - }, - [setUrlQuery] - ); - - const onSort = useCallback( - (sort) => { - setUrlQuery({ - sort, - }); - }, - [setUrlQuery] - ); - - const setTableOptions = useCallback( - ({ page, sort }) => { - setPageSize(page.size); - setUrlQuery({ - sort, - pageIndex: page.index, - }); - }, - [setUrlQuery, setPageSize] - ); - - /** - * Page URL query to ES query - */ - const baseEsQuery = useBaseEsQuery({ - dataView, - filters: urlQuery.filters, - query: urlQuery.query, - }); - - const handleUpdateQuery = useCallback( - (query) => { - setUrlQuery({ ...query, pageIndex: 0 }); - }, - [setUrlQuery] - ); - - const getRowsFromPages = (data: Array<{ page: DataTableRecord[] }> | undefined) => - data - ?.map(({ page }: { page: DataTableRecord[] }) => { - return page; - }) - .flat() || []; - - return { - setUrlQuery, - sort: urlQuery.sort, - filters: urlQuery.filters, - query: baseEsQuery.query, - queryError: baseEsQuery.error, - pageIndex: urlQuery.pageIndex, - urlQuery, - setTableOptions, - handleUpdateQuery, - pageSize, - setPageSize, - onChangeItemsPerPage, - onChangePage, - onSort, - onResetFilters, - columnsLocalStorageKey: columnsLocalStorageKey || LOCAL_STORAGE_DATA_TABLE_COLUMNS_KEY, - getRowsFromPages, - }; -}; diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts deleted file mode 100644 index a74abccba1e18b..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/utils.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useCallback, useMemo } from 'react'; -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 { - FindingsBaseESQueryConfig, - FindingsBaseProps, - FindingsBaseURLQuery, -} from '../../types'; - -const getBaseQuery = ({ - dataView, - query, - filters, - config, -}: FindingsBaseURLQuery & FindingsBaseProps & FindingsBaseESQueryConfig) => { - try { - return { - query: buildEsQuery(dataView, query, filters, config), // will throw for malformed query - }; - } catch (error) { - return { - query: undefined, - error: error instanceof Error ? error : new Error('Unknown Error'), - }; - } -}; - -type TablePagination = NonNullable['pagination']>; - -export const getPaginationTableParams = ( - params: TablePagination & Pick, 'pageIndex' | 'pageSize'>, - pageSizeOptions = [10, 25, 100], - showPerPageOptions = true -): Required => ({ - ...params, - pageSizeOptions, - showPerPageOptions, -}); - -export const getPaginationQuery = ({ - pageIndex, - pageSize, -}: Required>) => ({ - from: pageIndex * pageSize, - size: pageSize, -}); - -export const useBaseEsQuery = ({ - dataView, - filters, - query, -}: FindingsBaseURLQuery & FindingsBaseProps) => { - const { - notifications: { toasts }, - 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, config }), - [dataView, filters, query, config] - ); - - /** - * Sync filters with the URL query - */ - useEffect(() => { - filterManager.setAppFilters(filters); - queryString.setQuery(query); - }, [filters, filterManager, queryString, query]); - - const handleMalformedQueryError = () => { - const error = baseEsQuery.error; - if (error) { - toasts.addError(error, { - title: i18n.translate('xpack.csp.findings.search.queryErrorToastMessage', { - defaultMessage: 'Query Error', - }), - toastLifeTimeMs: 1000 * 5, - }); - } - }; - - useEffect(handleMalformedQueryError, [baseEsQuery.error, toasts]); - - return baseEsQuery; -}; - -export const usePersistedQuery = (getter: ({ filters, query }: FindingsBaseURLQuery) => T) => { - const { - data: { - query: { filterManager, queryString }, - }, - } = useKibana().services; - - return useCallback( - () => - getter({ - filters: filterManager.getAppFilters(), - query: queryString.getQuery() as Query, - }), - [getter, filterManager, queryString] - ); -}; - -export const getDefaultQuery = ({ query, filters }: any): any => ({ - query, - filters, - sort: { field: '@timestamp', direction: 'desc' }, - pageIndex: 0, -}); diff --git a/x-pack/plugins/cloud_security_posture/public/common/types.ts b/x-pack/plugins/cloud_security_posture/public/common/types.ts index ac483445407e48..d402ea29390627 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/types.ts @@ -5,7 +5,6 @@ * 2.0. */ import type { Criteria } from '@elastic/eui'; -import type { DataView } from '@kbn/data-views-plugin/common'; import type { BoolQuery, Filter, Query, EsQueryConfig } from '@kbn/es-query'; import { CspFinding } from '../../common/schemas/csp_finding'; @@ -20,12 +19,6 @@ export interface FindingsBaseURLQuery { nonPersistedFilters?: Filter[]; } -export interface FindingsBaseProps { - dataView: DataView; - dataViewRefetch?: () => void; - dataViewIsRefetching?: boolean; -} - export interface FindingsBaseESQueryConfig { config: EsQueryConfig; } diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.test.tsx index 7ddbe28a7da077..0fba4f27ed23ad 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.test.tsx @@ -6,6 +6,7 @@ */ import { render } from '@testing-library/react'; import React from 'react'; +import { DataViewContext } from '../../common/contexts/data_view_context'; import { TestProvider } from '../../test/test_provider'; import { CloudSecurityDataTable, CloudSecurityDataTableProps } from './cloud_security_data_table'; @@ -47,7 +48,6 @@ const mockCloudPostureDataTable = { const renderDataTable = (props: Partial = {}) => { const defaultProps: CloudSecurityDataTableProps = { - dataView: mockDataView, isLoading: false, defaultColumns: mockDefaultColumns, rows: [], @@ -60,7 +60,9 @@ const renderDataTable = (props: Partial = {}) => { return render( - + + + ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx index 3f0c3da73a9862..53ed78b6e9b781 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx @@ -6,7 +6,6 @@ */ import React, { useState, useMemo } from 'react'; import { UnifiedDataTableSettings, useColumns } from '@kbn/unified-data-table'; -import { type DataView } from '@kbn/data-views-plugin/common'; import { UnifiedDataTable, DataLoadingState } from '@kbn/unified-data-table'; import { CellActionsProvider } from '@kbn/cell-actions'; import { SHOW_MULTIFIELDS, SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils'; @@ -22,6 +21,7 @@ import { EmptyState } from '../empty_state'; import { MAX_FINDINGS_TO_LOAD } from '../../common/constants'; import { useStyles } from './use_styles'; import { AdditionalControls } from './additional_controls'; +import { useDataViewContext } from '../../common/contexts/data_view_context'; export interface CloudSecurityDefaultColumn { id: string; @@ -41,7 +41,6 @@ const useNewFieldsApi = true; const controlColumnIds = ['openDetails']; export interface CloudSecurityDataTableProps { - dataView: DataView; isLoading: boolean; defaultColumns: CloudSecurityDefaultColumn[]; rows: DataTableRecord[]; @@ -77,21 +76,10 @@ export interface CloudSecurityDataTableProps { /** * Height override for the data grid. */ - height?: number; - /** - * Callback Function when the DataView field is edited. - * Required to enable editing of the field in the data grid. - */ - dataViewRefetch?: () => void; - /** - * Flag to indicate if the data view is refetching. - * Required for smoothing re-rendering the DataTable columns. - */ - dataViewIsRefetching?: boolean; + height?: number | string; } export const CloudSecurityDataTable = ({ - dataView, isLoading, defaultColumns, rows, @@ -103,8 +91,6 @@ export const CloudSecurityDataTable = ({ customCellRenderer, groupSelectorComponent, height, - dataViewRefetch, - dataViewIsRefetching, ...rest }: CloudSecurityDataTableProps) => { const { @@ -133,6 +119,8 @@ export const CloudSecurityDataTable = ({ } ); + const { dataView, dataViewIsRefetching, dataViewRefetch } = useDataViewContext(); + const [expandedDoc, setExpandedDoc] = useState(undefined); const renderDocumentView = (hit: DataTableRecord) => @@ -245,7 +233,7 @@ export const CloudSecurityDataTable = ({ // Change the height of the grid to fit the page // If there are filters, leave space for the filter bar // Todo: Replace this component with EuiAutoSizer - height: height ?? `calc(100vh - ${filters?.length > 0 ? 443 : 403}px)`, + height: height ?? `calc(100vh - ${filters?.length > 0 ? 454 : 414}px)`, }; const rowHeightState = 0; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/fields_selector/fields_selector_table.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/fields_selector/fields_selector_table.tsx index bae971749bc789..0afd4332c41db2 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/fields_selector/fields_selector_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/fields_selector/fields_selector_table.tsx @@ -73,7 +73,7 @@ export const FieldsSelectorTable = ({ return dataView.fields .getAll() .filter((field) => { - return field.name !== '@timestamp' && field.name !== '_index' && field.visualizable; + return field.name !== '_index' && field.visualizable; }) .map((field) => ({ id: field.name, diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/index.ts b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/index.ts index 35a321d06119d5..84353541e8ad8d 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/index.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/index.ts @@ -7,3 +7,6 @@ export { useCloudSecurityGrouping } from './use_cloud_security_grouping'; export { CloudSecurityGrouping } from './cloud_security_grouping'; +export { firstNonNullValue } from './utils/first_non_null_value'; +export { NullGroup } from './null_group'; +export { LoadingGroup } from './loading_group'; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/loading_group.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/loading_group.tsx new file mode 100644 index 00000000000000..8774095be67557 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/loading_group.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSkeletonTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; + +export const LoadingGroup = () => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/null_group.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/null_group.tsx new file mode 100644 index 00000000000000..bf06d15e61a2a2 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/null_group.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiIconTip } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; + +export const NullGroup = ({ + title, + field, + unit, +}: { + title: string; + field: string; + unit: string; +}) => { + return ( + + {title} + + + + + ), + field: {field}, + unit, + }} + /> + + } + position="right" + /> + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts index c59d3821445240..23fd8267e5d761 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/use_cloud_security_grouping.ts @@ -19,7 +19,6 @@ import { FindingsBaseURLQuery } from '../../common/types'; import { useBaseEsQuery, usePersistedQuery } from '../../common/hooks/use_cloud_posture_data_table'; const DEFAULT_PAGE_SIZE = 10; -const GROUPING_ID = 'cspLatestFindings'; const MAX_GROUPING_LEVELS = 1; /* @@ -33,6 +32,7 @@ export const useCloudSecurityGrouping = ({ unit, groupPanelRenderer, groupStatsRenderer, + groupingLocalStorageKey, }: { dataView: DataView; groupingTitle: string; @@ -41,6 +41,7 @@ export const useCloudSecurityGrouping = ({ unit: (count: number) => string; groupPanelRenderer?: GroupPanelRenderer; groupStatsRenderer?: GroupStatsRenderer; + groupingLocalStorageKey: string; }) => { const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery); const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery); @@ -48,7 +49,6 @@ export const useCloudSecurityGrouping = ({ const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); const { query, error } = useBaseEsQuery({ - dataView, filters: urlQuery.filters, query: urlQuery.query, }); @@ -69,7 +69,7 @@ export const useCloudSecurityGrouping = ({ }, defaultGroupingOptions, fields: dataView.fields, - groupingId: GROUPING_ID, + groupingId: groupingLocalStorageKey, maxGroupingLevels: MAX_GROUPING_LEVELS, title: groupingTitle, onGroupChange: () => { diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/utils/first_non_null_value.test.ts b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/utils/first_non_null_value.test.ts new file mode 100644 index 00000000000000..5c332e6924ca16 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/utils/first_non_null_value.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { firstNonNullValue } from './first_non_null_value'; + +describe('firstNonNullValue', () => { + it('returns the value itself for non-null single value', () => { + expect(firstNonNullValue(5)).toBe(5); + }); + + it('returns undefined for a null single value', () => { + expect(firstNonNullValue(null)).toBeUndefined(); + }); + + it('returns undefined for an array of all null values', () => { + expect(firstNonNullValue([null, null, null])).toBeUndefined(); + }); + + it('returns the first non-null value in an array of mixed values', () => { + expect(firstNonNullValue([null, 7, 8])).toBe(7); + }); + + it('returns the first value in an array of all non-null values', () => { + expect(firstNonNullValue([3, 4, 5])).toBe(3); + }); + + it('returns undefined for an empty array', () => { + expect(firstNonNullValue([])).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/utils/first_non_null_value.ts b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/utils/first_non_null_value.ts new file mode 100644 index 00000000000000..a8c5da0500e8a0 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_grouping/utils/first_non_null_value.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ECSField } from '@kbn/securitysolution-grouping/src'; + +/** + * Return first non-null value. If the field contains an array, this will return the first value that isn't null. If the field isn't an array it'll be returned unless it's null. + */ +export function firstNonNullValue(valueOrCollection: ECSField): T | undefined { + if (valueOrCollection === null) { + return undefined; + } else if (Array.isArray(valueOrCollection)) { + for (const value of valueOrCollection) { + if (value !== null) { + return value; + } + } + } else { + return valueOrCollection; + } +} diff --git a/x-pack/plugins/cloud_security_posture/public/components/vulnerability_badges.tsx b/x-pack/plugins/cloud_security_posture/public/components/vulnerability_badges.tsx index 20b1326d655264..ff8924833a294a 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/vulnerability_badges.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/vulnerability_badges.tsx @@ -14,15 +14,16 @@ import { VulnSeverity } from '../../common/types_old'; import { VULNERABILITIES_CVSS_SCORE_BADGE_SUBJ } from './test_subjects'; interface CVSScoreBadgeProps { - score: float; + score?: float; version?: string; } interface SeverityStatusBadgeProps { - severity: VulnSeverity; + severity?: VulnSeverity; } export const CVSScoreBadge = ({ score, version }: CVSScoreBadgeProps) => { + if (!score) return null; const color = getCvsScoreColor(score); const versionDisplay = version ? `v${version.split('.')[0]}` : null; return ( @@ -56,6 +57,7 @@ export const CVSScoreBadge = ({ score, version }: CVSScoreBadgeProps) => { }; export const SeverityStatusBadge = ({ severity }: SeverityStatusBadgeProps) => { + if (!severity) return null; const color = getSeverityStatusColor(severity); return ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx index 7e8bbfeedb832a..60de4432282811 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx @@ -14,8 +14,8 @@ import { NoFindingsStates } from '../../components/no_findings_states'; import { CloudPosturePage } from '../../components/cloud_posture_page'; import { useLatestFindingsDataView } from '../../common/api/use_latest_findings_data_view'; import { cloudPosturePages, findingsNavigation } from '../../common/navigation/constants'; -import { FindingsByResourceContainer } from './latest_findings_by_resource/findings_by_resource_container'; import { LatestFindingsContainer } from './latest_findings/latest_findings_container'; +import { DataViewContext } from '../../common/contexts/data_view_context'; export const Configurations = () => { const location = useLocation(); @@ -31,6 +31,12 @@ export const Configurations = () => { if (!hasConfigurationFindings) return ; + const dataViewContextValue = { + dataView: dataViewQuery.data!, + dataViewRefetch: dataViewQuery.refetch, + dataViewIsRefetching: dataViewQuery.isRefetching, + }; + return ( @@ -50,18 +56,12 @@ export const Configurations = () => { path={findingsNavigation.findings_default.path} render={() => ( - + + + )} /> - } - /> } /> diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts index e2e4585906bae8..3d8200a144bd5d 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/constants.ts @@ -77,8 +77,6 @@ export const groupingTitle = i18n.translate('xpack.csp.findings.latestFindings.g defaultMessage: 'Group findings by', }); -export const DEFAULT_TABLE_HEIGHT = 512; - export const getDefaultQuery = ({ query, filters, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index e070847b6df554..11c60718b29f85 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -4,40 +4,29 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useCallback } from 'react'; +import React from 'react'; import { Filter } from '@kbn/es-query'; import { EuiSpacer } from '@elastic/eui'; +import { DEFAULT_GROUPING_TABLE_HEIGHT } from '../../../common/constants'; import { EmptyState } from '../../../components/empty_state'; import { CloudSecurityGrouping } from '../../../components/cloud_security_grouping'; -import type { FindingsBaseProps } from '../../../common/types'; import { FindingsSearchBar } from '../layout/findings_search_bar'; -import { DEFAULT_TABLE_HEIGHT } from './constants'; import { useLatestFindingsGrouping } from './use_latest_findings_grouping'; import { LatestFindingsTable } from './latest_findings_table'; import { groupPanelRenderer, groupStatsRenderer } from './latest_findings_group_renderer'; import { FindingsDistributionBar } from '../layout/findings_distribution_bar'; import { ErrorCallout } from '../layout/error_callout'; -export const LatestFindingsContainer = ({ - dataView, - dataViewRefetch, - dataViewIsRefetching, -}: FindingsBaseProps) => { - const renderChildComponent = useCallback( - (groupFilters: Filter[]) => { - return ( - - ); - }, - [dataView, dataViewIsRefetching, dataViewRefetch] - ); +export const LatestFindingsContainer = () => { + const renderChildComponent = (groupFilters: Filter[]) => { + return ( + + ); + }; const { isGroupSelected, @@ -57,12 +46,12 @@ export const LatestFindingsContainer = ({ onDistributionBarClick, totalFailedFindings, isEmptyResults, - } = useLatestFindingsGrouping({ dataView, groupPanelRenderer, groupStatsRenderer }); + } = useLatestFindingsGrouping({ groupPanelRenderer, groupStatsRenderer }); if (error || isEmptyResults) { return ( <> - + {error && } {isEmptyResults && } @@ -72,7 +61,7 @@ export const LatestFindingsContainer = ({ if (isGroupSelected) { return ( <> - +
- - + + ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx index d0684452fb23a9..fe8536eaf0f697 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_group_renderer.tsx @@ -8,23 +8,20 @@ import { EuiBadge, EuiFlexGroup, EuiFlexItem, - EuiIconTip, - EuiSkeletonTitle, EuiText, EuiTextBlockTruncate, EuiToolTip, useEuiTheme, } from '@elastic/eui'; import { css } from '@emotion/react'; -import { - ECSField, - GroupPanelRenderer, - RawBucket, - StatRenderer, -} from '@kbn/securitysolution-grouping/src'; +import { GroupPanelRenderer, RawBucket, StatRenderer } from '@kbn/securitysolution-grouping/src'; import React from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; +import { + NullGroup, + LoadingGroup, + firstNonNullValue, +} from '../../../components/cloud_security_grouping'; import { getAbbreviatedNumber } from '../../../common/utils/get_abbreviated_number'; import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon'; import { ComplianceScoreBar } from '../../../components/compliance_score_bar'; @@ -32,67 +29,6 @@ import { FindingsGroupingAggregation } from './use_grouped_findings'; import { GROUPING_OPTIONS, NULL_GROUPING_MESSAGES, NULL_GROUPING_UNIT } from './constants'; import { FINDINGS_GROUPING_COUNTER } from '../test_subjects'; -/** - * Return first non-null value. If the field contains an array, this will return the first value that isn't null. If the field isn't an array it'll be returned unless it's null. - */ -export function firstNonNullValue(valueOrCollection: ECSField): T | undefined { - if (valueOrCollection === null) { - return undefined; - } else if (Array.isArray(valueOrCollection)) { - for (const value of valueOrCollection) { - if (value !== null) { - return value; - } - } - } else { - return valueOrCollection; - } -} - -const NullGroupComponent = ({ - title, - field, - unit = NULL_GROUPING_UNIT, -}: { - title: string; - field: string; - unit?: string; -}) => { - return ( - - {title} - - - - - ), - field: {field}, - unit, - }} - /> - - } - position="right" - /> - - ); -}; - export const groupPanelRenderer: GroupPanelRenderer = ( selectedGroup, bucket, @@ -100,20 +36,18 @@ export const groupPanelRenderer: GroupPanelRenderer isLoading ) => { if (isLoading) { - return ( - - - - ); + return ; } const benchmarkId = firstNonNullValue(bucket.benchmarkId?.buckets?.[0]?.key); + + const renderNullGroup = (title: string) => ( + + ); + switch (selectedGroup) { case GROUPING_OPTIONS.RESOURCE_NAME: return nullGroupMessage ? ( - + renderNullGroup(NULL_GROUPING_MESSAGES.RESOURCE_NAME) ) : ( @@ -146,7 +80,7 @@ export const groupPanelRenderer: GroupPanelRenderer ); case GROUPING_OPTIONS.RULE_NAME: return nullGroupMessage ? ( - + renderNullGroup(NULL_GROUPING_MESSAGES.RULE_NAME) ) : ( @@ -168,10 +102,7 @@ export const groupPanelRenderer: GroupPanelRenderer ); case GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME: return nullGroupMessage ? ( - + renderNullGroup(NULL_GROUPING_MESSAGES.CLOUD_ACCOUNT_NAME) ) : ( {benchmarkId && ( @@ -200,10 +131,7 @@ export const groupPanelRenderer: GroupPanelRenderer ); case GROUPING_OPTIONS.ORCHESTRATOR_CLUSTER_NAME: return nullGroupMessage ? ( - + renderNullGroup(NULL_GROUPING_MESSAGES.ORCHESTRATOR_CLUSTER_NAME) ) : ( {benchmarkId && ( @@ -232,7 +160,7 @@ export const groupPanelRenderer: GroupPanelRenderer ); default: return nullGroupMessage ? ( - + renderNullGroup(NULL_GROUPING_MESSAGES.DEFAULT) ) : ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx index 3adb10259871d0..7f215c4d49f99e 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx @@ -10,7 +10,6 @@ import { DataTableRecord } from '@kbn/discover-utils/types'; import { i18n } from '@kbn/i18n'; import { EuiDataGridCellValueElementProps, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React from 'react'; -import { FindingsBaseProps } from '../../../common/types'; import * as TEST_SUBJECTS from '../test_subjects'; import { FindingsDistributionBar } from '../layout/findings_distribution_bar'; import { ErrorCallout } from '../layout/error_callout'; @@ -22,14 +21,12 @@ import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge'; import { CspFinding } from '../../../../common/schemas/csp_finding'; import { FindingsRuleFlyout } from '../findings_flyout/findings_flyout'; -type LatestFindingsTableProps = FindingsBaseProps & { +interface LatestFindingsTableProps { groupSelectorComponent?: JSX.Element; height?: number; showDistributionBar?: boolean; nonPersistedFilters?: Filter[]; - dataViewRefetch?: () => void; - dataViewIsRefetching?: boolean; -}; +} /** * Type Guard for checking if the given source is a CspFinding @@ -84,13 +81,10 @@ const customCellRenderer = (rows: DataTableRecord[]) => ({ }); export const LatestFindingsTable = ({ - dataView, groupSelectorComponent, height, showDistributionBar = true, nonPersistedFilters, - dataViewRefetch, - dataViewIsRefetching, }: LatestFindingsTableProps) => { const { cloudPostureDataTable, @@ -104,7 +98,6 @@ export const LatestFindingsTable = ({ canShowDistributionBar, onDistributionBarClick, } = useLatestFindingsTable({ - dataView, getDefaultQuery, nonPersistedFilters, showDistributionBar, @@ -132,7 +125,6 @@ export const LatestFindingsTable = ({ )} )} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts index 5584b1eae08a6b..9968bb9c414bf0 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings.ts @@ -29,6 +29,7 @@ import { buildMutedRulesFilter } from '../../../../common/utils/rules_states'; interface UseFindingsOptions extends FindingsBaseEsQuery { sort: string[][]; enabled: boolean; + pageSize: number; } export interface FindingsGroupByNoneQuery { @@ -76,7 +77,7 @@ export const getFindingsQuery = ( must_not: mutedRulesFilterQuery, }, }, - ...(pageParam ? { search_after: pageParam } : {}), + ...(pageParam ? { from: pageParam } : {}), }; }; @@ -125,6 +126,12 @@ export const useLatestFindings = (options: UseFindingsOptions) => { } = useKibana().services; const { data: rulesStates } = useGetCspBenchmarkRulesStatesApi(); + /** + * We're using useInfiniteQuery in this case to allow the user to fetch more data (if available and up to 10k) + * useInfiniteQuery differs from useQuery because it accumulates and caches a chunk of data from the previous fetches into an array + * it uses the getNextPageParam to know if there are more pages to load and retrieve the position of + * the last loaded record to be used as a from parameter to fetch the next chunk of data. + */ return useInfiniteQuery( ['csp_findings', { params: options }], async ({ pageParam }) => { @@ -149,9 +156,11 @@ export const useLatestFindings = (options: UseFindingsOptions) => { enabled: options.enabled && !!rulesStates, keepPreviousData: true, onError: (err: Error) => showErrorToast(toasts, err), - getNextPageParam: (lastPage) => { - if (lastPage.page.length === 0) return undefined; - return lastPage.page[lastPage.page.length - 1].raw.sort; + getNextPageParam: (lastPage, allPages) => { + if (lastPage.page.length < options.pageSize) { + return undefined; + } + return allPages.length * options.pageSize; }, } ); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx index 7b1f10c406e155..d2386bbdd3493b 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_grouping.tsx @@ -14,7 +14,8 @@ import { parseGroupingQuery, } from '@kbn/securitysolution-grouping/src'; import { useMemo } from 'react'; -import { DataView } from '@kbn/data-views-plugin/common'; +import { LOCAL_STORAGE_FINDINGS_GROUPING_KEY } from '../../../common/constants'; +import { useDataViewContext } from '../../../common/contexts/data_view_context'; import { Evaluation } from '../../../../common/types_old'; import { LATEST_FINDINGS_RETENTION_POLICY } from '../../../../common/constants'; import { @@ -124,14 +125,14 @@ export const isFindingsRootGroupingAggregation = ( * for the findings page */ export const useLatestFindingsGrouping = ({ - dataView, groupPanelRenderer, groupStatsRenderer, }: { - dataView: DataView; groupPanelRenderer?: GroupPanelRenderer; groupStatsRenderer?: GroupStatsRenderer; }) => { + const { dataView } = useDataViewContext(); + const { activePageIndex, grouping, @@ -154,6 +155,7 @@ export const useLatestFindingsGrouping = ({ unit: FINDINGS_UNIT, groupPanelRenderer, groupStatsRenderer, + groupingLocalStorageKey: LOCAL_STORAGE_FINDINGS_GROUPING_KEY, }); const { data: rulesStates } = useGetCspBenchmarkRulesStatesApi(); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx index b60eefac2ac81c..a2c5ad544dadb2 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import { DataView } from '@kbn/data-views-plugin/common'; import { Filter } from '@kbn/es-query'; import { useMemo } from 'react'; +import { useDataViewContext } from '../../../common/contexts/data_view_context'; import { FindingsBaseURLQuery } from '../../../common/types'; import { Evaluation } from '../../../../common/types_old'; import { LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY } from '../../../common/constants'; @@ -18,25 +18,25 @@ import { useLatestFindings } from './use_latest_findings'; const columnsLocalStorageKey = 'cloudPosture:latestFindings:columns'; export const useLatestFindingsTable = ({ - dataView, getDefaultQuery, nonPersistedFilters, showDistributionBar, }: { - dataView: DataView; getDefaultQuery: (params: FindingsBaseURLQuery) => FindingsBaseURLQuery; nonPersistedFilters?: Filter[]; showDistributionBar?: boolean; }) => { + const { dataView } = useDataViewContext(); + const cloudPostureDataTable = useCloudPostureDataTable({ - dataView, paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, columnsLocalStorageKey, defaultQuery: getDefaultQuery, nonPersistedFilters, }); - const { query, sort, queryError, setUrlQuery, filters, getRowsFromPages } = cloudPostureDataTable; + const { query, sort, queryError, setUrlQuery, filters, getRowsFromPages, pageSize } = + cloudPostureDataTable; const { data, @@ -47,6 +47,7 @@ export const useLatestFindingsTable = ({ query, sort, enabled: !queryError, + pageSize, }); const rows = useMemo(() => getRowsFromPages(data?.pages), [data?.pages, getRowsFromPages]); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_container.tsx deleted file mode 100644 index 85095b149bce4d..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_container.tsx +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { Routes, Route } from '@kbn/shared-ux-router'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; -import { CspFinding } from '../../../../common/schemas/csp_finding'; -import type { Evaluation } from '../../../../common/types_old'; -import { FindingsSearchBar } from '../layout/findings_search_bar'; -import * as TEST_SUBJECTS from '../test_subjects'; -import { usePageSlice } from '../../../common/hooks/use_page_slice'; -import { FindingsByResourceQuery, useFindingsByResource } from './use_findings_by_resource'; -import { FindingsByResourceTable } from './findings_by_resource_table'; -import { getFilters } from '../utils/utils'; -import { LimitedResultsBar } from '../layout/findings_layout'; -import { FindingsGroupBySelector } from '../layout/findings_group_by_selector'; -import { findingsNavigation } from '../../../common/navigation/constants'; -import { ResourceFindings } from './resource_findings/resource_findings_container'; -import { ErrorCallout } from '../layout/error_callout'; -import { CurrentPageOfTotal, FindingsDistributionBar } from '../layout/findings_distribution_bar'; -import { LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY } from '../../../common/constants'; -import type { FindingsBaseURLQuery, FindingsBaseProps } from '../../../common/types'; -import { useCloudPostureTable } from '../../../common/hooks/use_cloud_posture_table'; -import { useLimitProperties } from '../../../common/utils/get_limit_properties'; -import { getPaginationTableParams } from '../../../common/hooks/use_cloud_posture_table/utils'; - -const getDefaultQuery = ({ - query, - filters, -}: FindingsBaseURLQuery): FindingsBaseURLQuery & FindingsByResourceQuery => ({ - query, - filters, - pageIndex: 0, - sort: { field: 'compliance_score' as keyof CspFinding, direction: 'asc' }, -}); - -/** - * @deprecated: This component is deprecated and will be removed in the next release. - */ -export const FindingsByResourceContainer = ({ dataView }: FindingsBaseProps) => ( - - ( - - - - )} - /> - ( - - - - )} - /> - -); - -/** - * @deprecated: This component is deprecated and will be removed in the next release. - */ -const LatestFindingsByResource = ({ dataView }: FindingsBaseProps) => { - const { queryError, query, pageSize, setTableOptions, urlQuery, setUrlQuery, onResetFilters } = - useCloudPostureTable({ - dataView, - defaultQuery: getDefaultQuery, - paginationLocalStorageKey: LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY, - }); - - /** - * Page ES query result - */ - const findingsGroupByResource = useFindingsByResource({ - sortDirection: urlQuery.sort.direction, - query, - enabled: !queryError, - }); - - const error = findingsGroupByResource.error || queryError; - - const slicedPage = usePageSlice(findingsGroupByResource.data?.page, urlQuery.pageIndex, pageSize); - - const { isLastLimitedPage, limitedTotalItemCount } = useLimitProperties({ - total: findingsGroupByResource.data?.total, - pageIndex: urlQuery.pageIndex, - pageSize, - }); - - const handleDistributionClick = (evaluation: Evaluation) => { - setUrlQuery({ - pageIndex: 0, - filters: getFilters({ - filters: urlQuery.filters, - dataView, - field: 'result.evaluation', - value: evaluation, - negate: false, - }), - }); - }; - - return ( -
- { - setUrlQuery({ ...newQuery, pageIndex: 0 }); - }} - loading={findingsGroupByResource.isFetching} - /> - - - {error && } - {!error && ( - <> - {findingsGroupByResource.isSuccess && !!findingsGroupByResource.data.page.length && ( - <> - - - - - - - - - - - - )} - - - setUrlQuery({ - pageIndex: 0, - filters: getFilters({ - filters: urlQuery.filters, - dataView, - field, - value, - negate, - }), - }) - } - /> - - )} - {isLastLimitedPage && } -
- ); -}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_table.test.tsx deleted file mode 100644 index f0a6375a66178f..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_table.test.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render, screen, within } from '@testing-library/react'; -import * as TEST_SUBJECTS from '../test_subjects'; -import { FindingsByResourceTable, getResourceId } from './findings_by_resource_table'; -import type { PropsOf } from '@elastic/eui'; -import Chance from 'chance'; -import { TestProvider } from '../../../test/test_provider'; -import type { FindingsByResourcePage } from './use_findings_by_resource'; -import { calculatePostureScore } from '../../../../common/utils/helpers'; -import { EMPTY_STATE_TEST_SUBJ } from '../../../components/test_subjects'; - -const chance = new Chance(); - -const getFakeFindingsByResource = (): FindingsByResourcePage => { - const failed = chance.natural(); - const passed = chance.natural(); - const total = failed + passed; - const [resourceName, resourceSubtype, ruleBenchmarkName, ...cisSections] = chance.unique( - chance.word, - 5 - ); - - return { - belongs_to: chance.guid(), - resource_id: chance.guid(), - 'resource.name': resourceName, - 'resource.sub_type': resourceSubtype, - 'rule.section': cisSections, - 'rule.benchmark.name': ruleBenchmarkName, - compliance_score: passed / total, - findings: { - failed_findings: failed, - passed_findings: passed, - normalized: passed / total, - total_findings: total, - }, - }; -}; - -type TableProps = PropsOf; - -describe('', () => { - it('renders the zero state when status success and data has a length of zero ', async () => { - const props: TableProps = { - loading: false, - items: [], - pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 0 }, - sorting: { - sort: { field: 'compliance_score', direction: 'desc' }, - }, - setTableOptions: jest.fn(), - onAddFilter: jest.fn(), - onResetFilters: jest.fn(), - }; - - render( - - - - ); - - expect(screen.getByTestId(EMPTY_STATE_TEST_SUBJ)).toBeInTheDocument(); - }); - - it('renders the table with provided items', () => { - const data = Array.from({ length: 10 }, getFakeFindingsByResource); - - const props: TableProps = { - loading: false, - items: data, - pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 0 }, - sorting: { - sort: { field: 'compliance_score', direction: 'desc' }, - }, - setTableOptions: jest.fn(), - onAddFilter: jest.fn(), - onResetFilters: jest.fn(), - }; - - render( - - - - ); - - data.forEach((item) => { - const row = screen.getByTestId( - TEST_SUBJECTS.getFindingsByResourceTableRowTestId(getResourceId(item)) - ); - expect(row).toBeInTheDocument(); - expect(within(row).getByText(item.resource_id || '')).toBeInTheDocument(); - if (item['resource.name']) - expect(within(row).getByText(item['resource.name'])).toBeInTheDocument(); - if (item['resource.sub_type']) - expect(within(row).getByText(item['resource.sub_type'])).toBeInTheDocument(); - expect( - within(row).getByText( - `${calculatePostureScore( - item.findings.passed_findings, - item.findings.failed_findings - ).toFixed(0)}%` - ) - ).toBeInTheDocument(); - }); - }); -}); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_table.tsx deleted file mode 100644 index 71c4219d3b8523..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/findings_by_resource_table.tsx +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { useMemo } from 'react'; -import { - EuiBasicTable, - type EuiTableFieldDataColumnType, - type CriteriaWithPagination, - type Pagination, - EuiToolTip, - EuiBasicTableProps, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import numeral from '@elastic/numeral'; -import { generatePath, Link } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { ColumnNameWithTooltip } from '../../../components/column_name_with_tooltip'; -import { ComplianceScoreBar } from '../../../components/compliance_score_bar'; -import * as TEST_SUBJECTS from '../test_subjects'; -import type { FindingsByResourcePage } from './use_findings_by_resource'; -import { findingsNavigation } from '../../../common/navigation/constants'; -import { - createColumnWithFilters, - type OnAddFilter, - baseFindingsColumns, -} from '../layout/findings_layout'; -import { EmptyState } from '../../../components/empty_state'; - -/** - * @deprecated: This function is deprecated and will be removed in the next release. - * use getAbbreviatedNumber from x-pack/plugins/cloud_security_posture/public/common/utils/get_abbreviated_number.ts - */ -export const formatNumber = (value: number) => - value < 1000 ? value : numeral(value).format('0.0a'); - -type Sorting = Required>['sorting']; - -interface Props { - items: FindingsByResourcePage[]; - loading: boolean; - pagination: Pagination; - sorting: Sorting; - setTableOptions(options: CriteriaWithPagination): void; - onAddFilter: OnAddFilter; - onResetFilters: () => void; -} - -/** - * @deprecated: This function is deprecated and will be removed in the next release. - */ -export const getResourceId = (resource: FindingsByResourcePage) => { - const sections = resource['rule.section'] || []; - return [resource.resource_id, ...sections].join('/'); -}; - -/** - * @deprecated: This component is deprecated and will be removed in the next release. - */ -const FindingsByResourceTableComponent = ({ - items, - loading, - pagination, - sorting, - setTableOptions, - onAddFilter, - onResetFilters, -}: Props) => { - const getRowProps = (row: FindingsByResourcePage) => ({ - 'data-test-subj': TEST_SUBJECTS.getFindingsByResourceTableRowTestId(getResourceId(row)), - }); - - const getNonSortableColumn = (column: EuiTableFieldDataColumnType) => ({ - ...column, - sortable: false, - }); - - const columns = useMemo( - () => [ - { - ...getNonSortableColumn(findingsByResourceColumns.resource_id), - ['data-test-subj']: TEST_SUBJECTS.FINDINGS_BY_RESOURCE_TABLE_RESOURCE_ID_COLUMN, - }, - createColumnWithFilters( - getNonSortableColumn(findingsByResourceColumns['resource.sub_type']), - { onAddFilter } - ), - createColumnWithFilters(getNonSortableColumn(findingsByResourceColumns['resource.name']), { - onAddFilter, - }), - createColumnWithFilters( - getNonSortableColumn(findingsByResourceColumns['rule.benchmark.name']), - { onAddFilter } - ), - getNonSortableColumn(findingsByResourceColumns.belongs_to), - findingsByResourceColumns.compliance_score, - ], - [onAddFilter] - ); - - if (!loading && !items.length) { - return ; - } - - return ( - - ); -}; - -const baseColumns: Array> = [ - { - ...baseFindingsColumns['resource.id'], - field: 'resource_id', - width: '15%', - render: (resourceId: FindingsByResourcePage['resource_id']) => { - if (!resourceId) return; - - return ( - - {resourceId} - - ); - }, - }, - baseFindingsColumns['resource.sub_type'], - baseFindingsColumns['resource.name'], - baseFindingsColumns['rule.benchmark.name'], - { - field: 'rule.section', - truncateText: true, - name: ( - - ), - render: (sections: string[]) => { - const items = sections.join(', '); - return ( - - <>{items} - - ); - }, - }, - { - field: 'belongs_to', - name: ( - - ), - truncateText: true, - }, - { - field: 'compliance_score', - width: '150px', - truncateText: true, - sortable: true, - name: ( - - ), - render: (complianceScore: FindingsByResourcePage['compliance_score'], data) => ( - - ), - dataType: 'number', - }, -]; - -type BaseFindingColumnName = typeof baseColumns[number]['field']; - -/** - * @deprecated: This function is deprecated and will be removed in the next release. - */ -export const findingsByResourceColumns = Object.fromEntries( - baseColumns.map((column) => [column.field, column]) -) as Record; - -/** - * @deprecated: This component is deprecated and will be removed in the next release. - */ -export const FindingsByResourceTable = React.memo(FindingsByResourceTableComponent); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_container.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_container.test.tsx deleted file mode 100644 index 95eb978f8e1ca1..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_container.test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { render } from '@testing-library/react'; -import { TestProvider } from '../../../../test/test_provider'; -import { useResourceFindings } from './use_resource_findings'; -import { FindingsBaseProps } from '../../../../common/types'; -import { ResourceFindings } from './resource_findings_container'; - -jest.mock('./use_resource_findings', () => ({ - useResourceFindings: jest.fn().mockReturnValue({ - data: undefined, - error: false, - }), -})); - -describe('', () => { - it('should fetch resources with the correct parameters', async () => { - const props: FindingsBaseProps = { - dataView: {} as any, - }; - - render( - - - - ); - - expect(useResourceFindings).toHaveBeenNthCalledWith(1, { - enabled: true, - query: { - bool: { - filter: [], - must: [], - must_not: [], - should: [], - }, - }, - resourceId: 'undefined', - sort: { - direction: 'asc', - field: 'result.evaluation', - }, - }); - }); -}); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_container.tsx deleted file mode 100644 index bc6e67b887096f..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_container.tsx +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { useCallback } from 'react'; -import { - EuiSpacer, - EuiButtonEmpty, - type EuiDescriptionListProps, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { Link, useParams } from 'react-router-dom'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { generatePath } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { CspInlineDescriptionList } from '../../../../components/csp_inline_description_list'; -import type { Evaluation } from '../../../../../common/types_old'; -import { CspFinding } from '../../../../../common/schemas/csp_finding'; -import { CloudPosturePageTitle } from '../../../../components/cloud_posture_page_title'; -import * as TEST_SUBJECTS from '../../test_subjects'; -import { LimitedResultsBar, PageTitle, PageTitleText } from '../../layout/findings_layout'; -import { findingsNavigation } from '../../../../common/navigation/constants'; -import { ResourceFindingsQuery, useResourceFindings } from './use_resource_findings'; -import { usePageSlice } from '../../../../common/hooks/use_page_slice'; -import { getFilters } from '../../utils/utils'; -import { ResourceFindingsTable } from './resource_findings_table'; -import { FindingsSearchBar } from '../../layout/findings_search_bar'; -import { ErrorCallout } from '../../layout/error_callout'; -import { - CurrentPageOfTotal, - FindingsDistributionBar, -} from '../../layout/findings_distribution_bar'; -import { LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY } from '../../../../common/constants'; -import type { FindingsBaseURLQuery, FindingsBaseProps } from '../../../../common/types'; -import { useCloudPostureTable } from '../../../../common/hooks/use_cloud_posture_table'; -import { useLimitProperties } from '../../../../common/utils/get_limit_properties'; -import { getPaginationTableParams } from '../../../../common/hooks/use_cloud_posture_table/utils'; - -const getDefaultQuery = ({ - query, - filters, -}: FindingsBaseURLQuery): FindingsBaseURLQuery & - ResourceFindingsQuery & { findingIndex: number } => ({ - query, - filters, - sort: { field: 'result.evaluation' as keyof CspFinding, direction: 'asc' }, - pageIndex: 0, - findingIndex: -1, -}); - -const BackToResourcesButton = () => ( - - - - - -); - -const getResourceFindingSharedValues = (sharedValues: { - resourceId: string; - resourceSubType: string; - resourceName: string; - clusterId: string; - cloudAccountName: string; -}): EuiDescriptionListProps['listItems'] => [ - { - title: i18n.translate('xpack.csp.findings.resourceFindingsSharedValues.resourceTypeTitle', { - defaultMessage: 'Resource Type', - }), - description: sharedValues.resourceSubType, - }, - { - title: i18n.translate('xpack.csp.findings.resourceFindingsSharedValues.resourceIdTitle', { - defaultMessage: 'Resource ID', - }), - description: sharedValues.resourceId, - }, - { - title: i18n.translate('xpack.csp.findings.resourceFindingsSharedValues.clusterIdTitle', { - defaultMessage: 'Cluster ID', - }), - description: sharedValues.clusterId, - }, - { - title: i18n.translate('xpack.csp.findings.resourceFindingsSharedValues.cloudAccountName', { - defaultMessage: 'Cloud Account Name', - }), - description: sharedValues.cloudAccountName, - }, -]; - -/** - * @deprecated: This component is deprecated and will be removed in the next release. - */ -export const ResourceFindings = ({ dataView }: FindingsBaseProps) => { - const params = useParams<{ resourceId: string }>(); - const decodedResourceId = decodeURIComponent(params.resourceId); - - const { - pageIndex, - sort, - query, - queryError, - pageSize, - setTableOptions, - urlQuery, - setUrlQuery, - onResetFilters, - } = useCloudPostureTable({ - dataView, - defaultQuery: getDefaultQuery, - paginationLocalStorageKey: LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY, - }); - - /** - * Page ES query result - */ - const resourceFindings = useResourceFindings({ - sort, - resourceId: decodedResourceId, - enabled: !queryError, - query, - }); - - const error = resourceFindings.error || queryError; - - const slicedPage = usePageSlice(resourceFindings.data?.page, urlQuery.pageIndex, pageSize); - - const { isLastLimitedPage, limitedTotalItemCount } = useLimitProperties({ - total: resourceFindings.data?.total, - pageIndex: urlQuery.pageIndex, - pageSize, - }); - - const handleDistributionClick = (evaluation: Evaluation) => { - setUrlQuery({ - pageIndex: 0, - filters: getFilters({ - filters: urlQuery.filters, - dataView, - field: 'result.evaluation', - value: evaluation, - negate: false, - }), - }); - }; - - const flyoutFindingIndex = urlQuery?.findingIndex; - - const pagination = getPaginationTableParams({ - pageSize, - pageIndex, - totalItemCount: limitedTotalItemCount, - }); - - const onOpenFlyout = useCallback( - (flyoutFinding: CspFinding) => { - setUrlQuery({ - findingIndex: slicedPage.findIndex( - (finding) => - finding.resource.id === flyoutFinding?.resource.id && - finding.rule.id === flyoutFinding?.rule.id - ), - }); - }, - [slicedPage, setUrlQuery] - ); - - const onCloseFlyout = () => - setUrlQuery({ - findingIndex: -1, - }); - - const onPaginateFlyout = useCallback( - (nextFindingIndex: number) => { - // the index of the finding in the current page - const newFindingIndex = nextFindingIndex % pageSize; - - // if the finding is not in the current page, we need to change the page - const flyoutPageIndex = Math.floor(nextFindingIndex / pageSize); - - setUrlQuery({ - pageIndex: flyoutPageIndex, - findingIndex: newFindingIndex, - }); - }, - [pageSize, setUrlQuery] - ); - - return ( -
- { - setUrlQuery({ ...newQuery, pageIndex: 0 }); - }} - loading={resourceFindings.isFetching} - /> - - - - - } - /> - - - {resourceFindings.data && ( - - )} - - - {error && } - {!error && ( - <> - {resourceFindings.isSuccess && !!resourceFindings.data.page.length && ( - <> - - - - - - - - - )} - - - setUrlQuery({ - pageIndex: 0, - filters: getFilters({ - filters: urlQuery.filters, - dataView, - field, - value, - negate, - }), - }) - } - /> - - )} - {isLastLimitedPage && } -
- ); -}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_table.test.tsx deleted file mode 100644 index b47366938db8d2..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_table.test.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { render, screen, within } from '@testing-library/react'; -import * as TEST_SUBJECTS from '../../test_subjects'; -import { ResourceFindingsTable, ResourceFindingsTableProps } from './resource_findings_table'; -import { TestProvider } from '../../../../test/test_provider'; - -import { capitalize } from 'lodash'; -import moment from 'moment'; -import { getFindingsFixture } from '../../../../test/fixtures/findings_fixture'; -import { EMPTY_STATE_TEST_SUBJ } from '../../../../components/test_subjects'; - -describe('', () => { - it('should render no findings empty state when status success and data has a length of zero ', async () => { - const resourceFindingsProps: ResourceFindingsTableProps = { - loading: false, - items: [], - pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 0 }, - sorting: { - sort: { field: '@timestamp', direction: 'desc' }, - }, - setTableOptions: jest.fn(), - onAddFilter: jest.fn(), - flyoutFindingIndex: -1, - onOpenFlyout: jest.fn(), - onCloseFlyout: jest.fn(), - onPaginateFlyout: jest.fn(), - onResetFilters: jest.fn(), - }; - - render( - - - - ); - - expect(screen.getByTestId(EMPTY_STATE_TEST_SUBJ)).toBeInTheDocument(); - }); - - it('should render resource finding table content when data has a non zero length', () => { - const data = Array.from({ length: 10 }, getFindingsFixture); - - const props: ResourceFindingsTableProps = { - loading: false, - items: data, - pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 0 }, - sorting: { - sort: { field: 'cluster_id', direction: 'desc' }, - }, - setTableOptions: jest.fn(), - onAddFilter: jest.fn(), - flyoutFindingIndex: -1, - onOpenFlyout: jest.fn(), - onCloseFlyout: jest.fn(), - onPaginateFlyout: jest.fn(), - onResetFilters: jest.fn(), - }; - - render( - - - - ); - - data.forEach((item, i) => { - const row = screen.getByTestId( - TEST_SUBJECTS.getResourceFindingsTableRowTestId(item.resource.id) - ); - const { evaluation } = item.result; - const evaluationStatusText = capitalize( - item.result.evaluation.slice(0, evaluation.length - 2) - ); - - expect(row).toBeInTheDocument(); - expect(within(row).queryByText(item.rule.name)).toBeInTheDocument(); - expect(within(row).queryByText(evaluationStatusText)).toBeInTheDocument(); - expect(within(row).queryByText(moment(item['@timestamp']).fromNow())).toBeInTheDocument(); - expect(within(row).queryByText(item.rule.section)).toBeInTheDocument(); - }); - }); -}); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_table.tsx deleted file mode 100644 index 4dd7070af88f12..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/resource_findings_table.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { useMemo } from 'react'; -import { - EuiBasicTable, - type CriteriaWithPagination, - type Pagination, - type EuiBasicTableColumn, - type EuiTableActionsColumnType, - type EuiBasicTableProps, - useEuiTheme, -} from '@elastic/eui'; -import { CspFinding } from '../../../../../common/schemas/csp_finding'; -import { - baseFindingsColumns, - createColumnWithFilters, - getExpandColumn, - type OnAddFilter, -} from '../../layout/findings_layout'; -import { FindingsRuleFlyout } from '../../findings_flyout/findings_flyout'; -import { getSelectedRowStyle } from '../../utils/utils'; -import * as TEST_SUBJECTS from '../../test_subjects'; -import { EmptyState } from '../../../../components/empty_state'; - -export interface ResourceFindingsTableProps { - items: CspFinding[]; - loading: boolean; - pagination: Pagination & { pageSize: number }; - sorting: Required>['sorting']; - setTableOptions(options: CriteriaWithPagination): void; - onAddFilter: OnAddFilter; - onPaginateFlyout: (pageIndex: number) => void; - onCloseFlyout: () => void; - onOpenFlyout: (finding: CspFinding) => void; - flyoutFindingIndex: number; - onResetFilters: () => void; -} - -const ResourceFindingsTableComponent = ({ - items, - loading, - pagination, - sorting, - setTableOptions, - onAddFilter, - onOpenFlyout, - flyoutFindingIndex, - onPaginateFlyout, - onCloseFlyout, - onResetFilters, -}: ResourceFindingsTableProps) => { - const { euiTheme } = useEuiTheme(); - - const selectedFinding = items[flyoutFindingIndex]; - - const getRowProps = (row: CspFinding) => ({ - style: getSelectedRowStyle(euiTheme, row, selectedFinding), - 'data-test-subj': TEST_SUBJECTS.getResourceFindingsTableRowTestId(row.resource.id), - }); - - const columns: [ - EuiTableActionsColumnType, - ...Array> - ] = useMemo( - () => [ - getExpandColumn({ onClick: onOpenFlyout }), - createColumnWithFilters(baseFindingsColumns['result.evaluation'], { onAddFilter }), - baseFindingsColumns['rule.benchmark.rule_number'], - createColumnWithFilters(baseFindingsColumns['rule.name'], { onAddFilter }), - createColumnWithFilters(baseFindingsColumns['rule.section'], { onAddFilter }), - baseFindingsColumns['@timestamp'], - ], - [onAddFilter, onOpenFlyout] - ); - - if (!loading && !items.length) { - return ; - } - - return ( - <> - - {selectedFinding && ( - - )} - - ); -}; - -/** - * @deprecated: This component is deprecated and will be removed in the next release. - */ -export const ResourceFindingsTable = React.memo(ResourceFindingsTableComponent); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/use_resource_findings.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/use_resource_findings.ts deleted file mode 100644 index 46a5e126656609..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/resource_findings/use_resource_findings.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { useQuery } from '@tanstack/react-query'; -import { lastValueFrom } from 'rxjs'; -import { IKibanaSearchRequest, IKibanaSearchResponse } from '@kbn/data-plugin/common'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { Pagination } from '@elastic/eui'; -import { number } from 'io-ts'; -import { getSafeKspmClusterIdRuntimeMapping } from '../../../../../common/runtime_mappings/get_safe_kspm_cluster_id_runtime_mapping'; -import { CspFinding } from '../../../../../common/schemas/csp_finding'; -import { getAggregationCount, getFindingsCountAggQuery } from '../../utils/utils'; -import { useKibana } from '../../../../common/hooks/use_kibana'; -import type { FindingsBaseEsQuery, Sort } from '../../../../common/types'; -import { CSP_LATEST_FINDINGS_DATA_VIEW } from '../../../../../common/constants'; -import { MAX_FINDINGS_TO_LOAD } from '../../../../common/constants'; -import { showErrorToast } from '../../../../common/utils/show_error_toast'; - -interface UseResourceFindingsOptions extends FindingsBaseEsQuery { - resourceId: string; - sort: Sort; - enabled: boolean; -} - -export interface ResourceFindingsQuery { - pageIndex: Pagination['pageIndex']; - sort: Sort; -} - -type ResourceFindingsRequest = IKibanaSearchRequest; -type ResourceFindingsResponse = IKibanaSearchResponse< - estypes.SearchResponse ->; - -export type ResourceFindingsResponseAggs = Record< - 'count' | 'clusterId' | 'resourceSubType' | 'resourceName' | 'cloudAccountName', - estypes.AggregationsMultiBucketAggregateBase< - estypes.AggregationsStringRareTermsBucketKeys | undefined - > ->; - -const getResourceFindingsQuery = ({ - query, - resourceId, - sort, -}: UseResourceFindingsOptions): estypes.SearchRequest => ({ - index: CSP_LATEST_FINDINGS_DATA_VIEW, - body: { - size: MAX_FINDINGS_TO_LOAD, - runtime_mappings: { - ...getSafeKspmClusterIdRuntimeMapping(), - }, - query: { - ...query, - bool: { - ...query?.bool, - filter: [...(query?.bool?.filter || []), { term: { 'resource.id': resourceId } }], - }, - }, - sort: [{ [sort.field]: sort.direction }], - aggs: { - ...getFindingsCountAggQuery(), - cloudAccountName: { - terms: { field: 'cloud.account.name' }, - }, - clusterId: { - terms: { field: 'safe_kspm_cluster_id' }, - }, - resourceSubType: { - terms: { field: 'resource.sub_type' }, - }, - resourceName: { - terms: { field: 'resource.name' }, - }, - }, - }, - ignore_unavailable: false, -}); - -/** - * @deprecated: This hook is deprecated and will be removed in the next release. - */ -export const useResourceFindings = (options: UseResourceFindingsOptions) => { - const { - data, - notifications: { toasts }, - } = useKibana().services; - - const params = { ...options }; - - return useQuery( - ['csp_resource_findings', { params }], - () => - lastValueFrom( - data.search.search({ - params: getResourceFindingsQuery(params), - }) - ), - { - enabled: options.enabled, - keepPreviousData: true, - select: ({ rawResponse: { hits, aggregations } }: ResourceFindingsResponse) => { - if (!aggregations) throw new Error('expected aggregations to exists'); - assertNonBucketsArray(aggregations.count?.buckets); - assertNonBucketsArray(aggregations.clusterId?.buckets); - assertNonBucketsArray(aggregations.resourceSubType?.buckets); - assertNonBucketsArray(aggregations.resourceName?.buckets); - assertNonBucketsArray(aggregations.cloudAccountName?.buckets); - - return { - page: hits.hits.map((hit) => hit._source!), - total: number.is(hits.total) ? hits.total : 0, - count: getAggregationCount(aggregations.count?.buckets), - clusterId: getFirstBucketKey(aggregations.clusterId?.buckets), - resourceSubType: getFirstBucketKey(aggregations.resourceSubType?.buckets), - resourceName: getFirstBucketKey(aggregations.resourceName?.buckets), - cloudAccountName: getFirstBucketKey(aggregations.cloudAccountName?.buckets), - }; - }, - onError: (err: Error) => showErrorToast(toasts, err), - } - ); -}; - -function assertNonBucketsArray(arr: unknown): asserts arr is T[] { - if (!Array.isArray(arr)) { - throw new Error('expected buckets to be an array'); - } -} - -const getFirstBucketKey = ( - buckets: Array -): string | undefined => buckets[0]?.key; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/use_findings_by_resource.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/use_findings_by_resource.ts deleted file mode 100644 index e4bbd955f6092c..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings_by_resource/use_findings_by_resource.ts +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { useQuery } from '@tanstack/react-query'; -import { lastValueFrom } from 'rxjs'; -import { IKibanaSearchRequest, IKibanaSearchResponse } from '@kbn/data-plugin/common'; -import type { Pagination } from '@elastic/eui'; -import { - AggregationsCardinalityAggregate, - AggregationsMultiBucketAggregateBase, - AggregationsMultiBucketBase, - AggregationsScriptedMetricAggregate, - AggregationsStringRareTermsBucketKeys, - AggregationsStringTermsBucketKeys, - SearchRequest, - SearchResponse, -} from '@elastic/elasticsearch/lib/api/types'; -import { CspFinding } from '../../../../common/schemas/csp_finding'; -import { getBelongsToRuntimeMapping } from '../../../../common/runtime_mappings/get_belongs_to_runtime_mapping'; -import { MAX_FINDINGS_TO_LOAD } from '../../../common/constants'; -import { useKibana } from '../../../common/hooks/use_kibana'; -import { showErrorToast } from '../../../common/utils/show_error_toast'; -import type { FindingsBaseEsQuery, Sort } from '../../../common/types'; -import { getAggregationCount, getFindingsCountAggQuery } from '../utils/utils'; -import { CSP_LATEST_FINDINGS_DATA_VIEW } from '../../../../common/constants'; - -interface UseFindingsByResourceOptions extends FindingsBaseEsQuery { - enabled: boolean; - sortDirection: Sort['direction']; -} - -// Maximum number of grouped findings, default limit in elasticsearch is set to 65,536 (ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-settings.html#search-settings-max-buckets) -const MAX_BUCKETS = 60 * 1000; - -export interface FindingsByResourceQuery { - pageIndex: Pagination['pageIndex']; - sort: Sort; -} - -type FindingsAggRequest = IKibanaSearchRequest; -type FindingsAggResponse = IKibanaSearchResponse>; - -export interface FindingsByResourcePage { - findings: { - failed_findings: number; - passed_findings: number; - normalized: number; - total_findings: number; - }; - compliance_score: number; - resource_id?: string; - belongs_to?: string; - 'resource.name'?: string; - 'resource.sub_type'?: string; - 'rule.benchmark.name'?: string; - 'rule.section'?: string[]; -} - -interface FindingsByResourceAggs { - resource_total: AggregationsCardinalityAggregate; - resources: AggregationsMultiBucketAggregateBase; - count: AggregationsMultiBucketAggregateBase; -} - -interface FindingsAggBucket extends AggregationsStringRareTermsBucketKeys { - failed_findings: AggregationsMultiBucketBase; - compliance_score: AggregationsScriptedMetricAggregate; - passed_findings: AggregationsMultiBucketBase; - name: AggregationsMultiBucketAggregateBase; - subtype: AggregationsMultiBucketAggregateBase; - belongs_to: AggregationsMultiBucketAggregateBase; - benchmarkName: AggregationsMultiBucketAggregateBase; - cis_sections: AggregationsMultiBucketAggregateBase; -} - -/** - * @deprecated: This hook is deprecated and will be removed in the next release. - */ -export const getFindingsByResourceAggQuery = ({ - query, - sortDirection, -}: UseFindingsByResourceOptions): SearchRequest => ({ - index: CSP_LATEST_FINDINGS_DATA_VIEW, - query, - size: 0, - runtime_mappings: getBelongsToRuntimeMapping(), - aggs: { - ...getFindingsCountAggQuery(), - resource_total: { cardinality: { field: 'resource.id' } }, - resources: { - terms: { field: 'resource.id', size: MAX_BUCKETS }, - aggs: { - name: { - terms: { field: 'resource.name', size: 1 }, - }, - subtype: { - terms: { field: 'resource.sub_type', size: 1 }, - }, - benchmarkName: { - terms: { field: 'rule.benchmark.name' }, - }, - cis_sections: { - terms: { field: 'rule.section' }, - }, - failed_findings: { - filter: { term: { 'result.evaluation': 'failed' } }, - }, - passed_findings: { - filter: { term: { 'result.evaluation': 'passed' } }, - }, - // this field is runtime generated - belongs_to: { - terms: { field: 'belongs_to', size: 1 }, - }, - compliance_score: { - bucket_script: { - buckets_path: { - passed: 'passed_findings>_count', - failed: 'failed_findings>_count', - }, - script: 'params.passed / (params.passed + params.failed)', - }, - }, - sort_by_compliance_score: { - bucket_sort: { - size: MAX_FINDINGS_TO_LOAD, - sort: [ - { - compliance_score: { order: sortDirection }, - _count: { order: 'desc' }, - _key: { order: 'asc' }, - }, - ], - }, - }, - }, - }, - }, - ignore_unavailable: false, -}); - -const getFirstKey = ( - buckets: AggregationsMultiBucketAggregateBase['buckets'] -): undefined | string => { - if (!!Array.isArray(buckets) && !!buckets.length) return buckets[0].key; -}; - -const getKeysList = ( - buckets: AggregationsMultiBucketAggregateBase['buckets'] -): undefined | string[] => { - if (!!Array.isArray(buckets) && !!buckets.length) return buckets.map((v) => v.key); -}; - -const createFindingsByResource = (resource: FindingsAggBucket): FindingsByResourcePage => ({ - resource_id: resource.key, - ['resource.name']: getFirstKey(resource.name.buckets), - ['resource.sub_type']: getFirstKey(resource.subtype.buckets), - ['rule.section']: getKeysList(resource.cis_sections.buckets), - ['rule.benchmark.name']: getFirstKey(resource.benchmarkName.buckets), - belongs_to: getFirstKey(resource.belongs_to.buckets), - compliance_score: resource.compliance_score.value, - findings: { - failed_findings: resource.failed_findings.doc_count, - normalized: - resource.doc_count > 0 ? resource.failed_findings.doc_count / resource.doc_count : 0, - total_findings: resource.doc_count, - passed_findings: resource.passed_findings.doc_count, - }, -}); - -/** - * @deprecated: This hook is deprecated and will be removed in the next release. - */ -export const useFindingsByResource = (options: UseFindingsByResourceOptions) => { - const { - data, - notifications: { toasts }, - } = useKibana().services; - - const params = { ...options }; - - return useQuery( - ['csp_findings_resource', { params }], - async () => { - const { - rawResponse: { aggregations }, - } = await lastValueFrom( - data.search.search({ - params: getFindingsByResourceAggQuery(params), - }) - ); - - if (!aggregations) throw new Error('Failed to aggregate by, missing resource id'); - - if ( - !Array.isArray(aggregations.resources.buckets) || - !Array.isArray(aggregations.count.buckets) - ) - throw new Error('Failed to group by, missing resource id'); - - const page = aggregations.resources.buckets.map(createFindingsByResource); - - return { - page, - total: aggregations.resource_total.value, - count: getAggregationCount(aggregations.count.buckets), - }; - }, - { - enabled: options.enabled, - keepPreviousData: true, - onError: (err: Error) => showErrorToast(toasts, err), - } - ); -}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_layout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_layout.tsx deleted file mode 100644 index 2a39550a3c7d4d..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_layout.tsx +++ /dev/null @@ -1,318 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { - EuiBottomBar, - EuiButtonIcon, - EuiSpacer, - EuiTableActionsColumnType, - EuiTableFieldDataColumnType, - EuiText, - EuiTitle, - EuiToolTip, - PropsOf, -} from '@elastic/eui'; -import { css } from '@emotion/react'; -import { i18n } from '@kbn/i18n'; -import { euiThemeVars } from '@kbn/ui-theme'; -import type { Serializable } from '@kbn/utility-types'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { FindingsByResourcePage } from '../latest_findings_by_resource/use_findings_by_resource'; -import { MAX_FINDINGS_TO_LOAD } from '../../../common/constants'; -import { TimestampTableCell } from '../../../components/timestamp_table_cell'; -import { ColumnNameWithTooltip } from '../../../components/column_name_with_tooltip'; -import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge'; -import { - FINDINGS_TABLE_CELL_ADD_FILTER, - FINDINGS_TABLE_CELL_ADD_NEGATED_FILTER, - FINDINGS_TABLE_EXPAND_COLUMN, -} from '../test_subjects'; - -export type OnAddFilter = (key: T, value: Serializable, negate: boolean) => void; - -export const PageTitle: React.FC = ({ children }) => ( - -
{children}
-
-); - -export const PageTitleText = ({ title }: { title: React.ReactNode }) => ( - -

{title}

-
-); - -export const getExpandColumn = ({ - onClick, -}: { - onClick(item: T): void; -}): EuiTableActionsColumnType => ({ - width: '40px', - actions: [ - { - 'data-test-subj': FINDINGS_TABLE_EXPAND_COLUMN, - name: i18n.translate('xpack.csp.expandColumnNameLabel', { defaultMessage: 'Expand' }), - description: i18n.translate('xpack.csp.expandColumnDescriptionLabel', { - defaultMessage: 'Expand', - }), - type: 'icon', - icon: 'expand', - onClick, - }, - ], -}); - -const baseColumns = [ - { - field: 'resource.id', - name: ( - - ), - truncateText: true, - width: '180px', - sortable: true, - render: (filename: string) => ( - - {filename} - - ), - }, - { - field: 'result.evaluation', - name: i18n.translate('xpack.csp.findings.findingsTable.findingsTableColumn.resultColumnLabel', { - defaultMessage: 'Result', - }), - width: '80px', - sortable: true, - render: (type: PropsOf['type']) => ( - - ), - }, - { - field: 'resource.sub_type', - name: i18n.translate( - 'xpack.csp.findings.findingsTable.findingsTableColumn.resourceTypeColumnLabel', - { defaultMessage: 'Resource Type' } - ), - sortable: true, - truncateText: true, - width: '10%', - }, - { - field: 'resource.name', - name: i18n.translate( - 'xpack.csp.findings.findingsTable.findingsTableColumn.resourceNameColumnLabel', - { defaultMessage: 'Resource Name' } - ), - sortable: true, - truncateText: true, - width: '12%', - render: (name: FindingsByResourcePage['resource.name']) => { - if (!name) return; - - return ( - - <>{name} - - ); - }, - }, - { - field: 'rule.name', - name: i18n.translate( - 'xpack.csp.findings.findingsTable.findingsTableColumn.ruleNameColumnLabel', - { defaultMessage: 'Rule Name' } - ), - sortable: true, - render: (name: string) => ( - - <>{name} - - ), - }, - { - field: 'rule.benchmark.rule_number', - name: i18n.translate( - 'xpack.csp.findings.findingsTable.findingsTableColumn.ruleNumberColumnLabel', - { - defaultMessage: 'Rule Number', - } - ), - sortable: true, - width: '120px', - }, - { - field: 'rule.benchmark.name', - name: ( - - ), - sortable: true, - truncateText: true, - }, - { - field: 'rule.section', - name: i18n.translate( - 'xpack.csp.findings.findingsTable.findingsTableColumn.ruleSectionColumnLabel', - { defaultMessage: 'CIS Section' } - ), - width: '150px', - sortable: true, - truncateText: true, - render: (section: string) => ( - - <>{section} - - ), - }, - { - field: '@timestamp', - align: 'right', - width: '10%', - name: i18n.translate( - 'xpack.csp.findings.findingsTable.findingsTableColumn.lastCheckedColumnLabel', - { defaultMessage: 'Last Checked' } - ), - truncateText: true, - sortable: true, - render: (timestamp: number) => , - }, -] as const; - -export const baseFindingsColumns = Object.fromEntries( - baseColumns.map((column) => [column.field, column]) -) as Record; - -export const createColumnWithFilters = ( - column: EuiTableFieldDataColumnType, - { onAddFilter }: { onAddFilter: OnAddFilter } -): EuiTableFieldDataColumnType => ({ - ...column, - render: (cellValue: Serializable, item: T) => ( - onAddFilter(column.field as string, cellValue, false)} - onAddNegateFilter={() => onAddFilter(column.field as string, cellValue, true)} - field={column.field as string} - > - {column.render?.(cellValue, item) || getCellValue(cellValue)} - - ), -}); - -const getCellValue = (value: unknown) => { - if (!value) return; - if (typeof value === 'string' || typeof value === 'number') return value; -}; - -const FilterableCell: React.FC<{ - onAddFilter(): void; - onAddNegateFilter(): void; - field: string; -}> = ({ children, onAddFilter, onAddNegateFilter, field }) => ( -
.__filter_buttons { - opacity: 1; - } - > .__filter_value { - max-width: calc(100% - calc(${euiThemeVars.euiSizeL} * 2)); - } - } - `} - > -
- {children} -
-
- - - - - - - -
-
-); - -export const LimitedResultsBar = () => ( - <> - - - - - - - -); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_search_bar.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_search_bar.tsx index 9b6e7bcb60c536..43077778c4fdf5 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_search_bar.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_search_bar.tsx @@ -8,9 +8,9 @@ import React, { useContext } from 'react'; import { css } from '@emotion/react'; import { EuiThemeComputed, useEuiTheme } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import type { DataView } from '@kbn/data-plugin/common'; import { i18n } from '@kbn/i18n'; import type { Filter } from '@kbn/es-query'; +import { useDataViewContext } from '../../../common/contexts/data_view_context'; import { SecuritySolutionContext } from '../../../application/security_solution_context'; import type { FindingsBaseURLQuery } from '../../../common/types'; import type { CspClientPluginStartDeps } from '../../../types'; @@ -25,13 +25,12 @@ interface FindingsSearchBarProps { } export const FindingsSearchBar = ({ - dataView, loading, setQuery, placeholder = i18n.translate('xpack.csp.findings.searchBar.searchPlaceholder', { defaultMessage: 'Search findings (eg. rule.section : "API Server" )', }), -}: FindingsSearchBarProps & { dataView: DataView }) => { +}: FindingsSearchBarProps) => { const { euiTheme } = useEuiTheme(); const { unifiedSearch: { @@ -41,6 +40,8 @@ export const FindingsSearchBar = ({ const securitySolutionContext = useContext(SecuritySolutionContext); + const { dataView } = useDataViewContext(); + let searchBarNode = (
({ + query, + filters, + sort: [ + [VULNERABILITY_FIELDS.SEVERITY, 'asc'], + [VULNERABILITY_FIELDS.SCORE_BASE, 'desc'], + ], +}); + +export const defaultColumns: CloudSecurityDefaultColumn[] = [ + { id: VULNERABILITY_FIELDS.VULNERABILITY_ID, width: 130 }, + { id: VULNERABILITY_FIELDS.SCORE_BASE, width: 80 }, + { id: VULNERABILITY_FIELDS.RESOURCE_NAME }, + { id: VULNERABILITY_FIELDS.RESOURCE_ID }, + { id: VULNERABILITY_FIELDS.SEVERITY, width: 100 }, + { id: VULNERABILITY_FIELDS.PACKAGE_NAME }, + { id: VULNERABILITY_FIELDS.PACKAGE_VERSION }, + { id: VULNERABILITY_FIELDS.PACKAGE_FIXED_VERSION }, +]; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_grouped_vulnerabilities.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_grouped_vulnerabilities.tsx new file mode 100644 index 00000000000000..fda12c4b06d412 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_grouped_vulnerabilities.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IKibanaSearchResponse } from '@kbn/data-plugin/public'; +import { GenericBuckets, GroupingQuery, RootAggregation } from '@kbn/securitysolution-grouping/src'; +import { useQuery } from '@tanstack/react-query'; +import { lastValueFrom } from 'rxjs'; +import { LATEST_VULNERABILITIES_INDEX_PATTERN } from '../../../../common/constants'; +import { useKibana } from '../../../common/hooks/use_kibana'; +import { showErrorToast } from '../../../common/utils/show_error_toast'; + +// Elasticsearch returns `null` when a sub-aggregation cannot be computed +type NumberOrNull = number | null; + +export interface VulnerabilitiesGroupingAggregation { + unitsCount?: { + value?: NumberOrNull; + }; + groupsCount?: { + value?: NumberOrNull; + }; + groupByFields?: { + buckets?: GenericBuckets[]; + }; + description?: { + buckets?: GenericBuckets[]; + }; + resourceId?: { + buckets?: GenericBuckets[]; + }; + isLoading?: boolean; +} + +export type VulnerabilitiesRootGroupingAggregation = + RootAggregation; + +export const getGroupedVulnerabilitiesQuery = (query: GroupingQuery) => ({ + ...query, + index: LATEST_VULNERABILITIES_INDEX_PATTERN, + size: 0, +}); + +export const useGroupedVulnerabilities = ({ + query, + enabled = true, +}: { + query: GroupingQuery; + enabled: boolean; +}) => { + const { + data, + notifications: { toasts }, + } = useKibana().services; + + return useQuery( + ['csp_grouped_vulnerabilities', { query }], + async () => { + const { + rawResponse: { aggregations }, + } = await lastValueFrom( + data.search.search< + {}, + IKibanaSearchResponse> + >({ + params: getGroupedVulnerabilitiesQuery(query), + }) + ); + + if (!aggregations) throw new Error('Failed to aggregate by, missing resource id'); + + return aggregations; + }, + { + onError: (err: Error) => showErrorToast(toasts, err), + enabled, + // This allows the UI to keep the previous data while the new data is being fetched + keepPreviousData: true, + } + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx index a3ae53a25f4d90..df9d5446ea9268 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useQuery } from '@tanstack/react-query'; +import { useInfiniteQuery } from '@tanstack/react-query'; import { lastValueFrom } from 'rxjs'; import type { IKibanaSearchRequest, IKibanaSearchResponse } from '@kbn/data-plugin/common'; import { number } from 'io-ts'; @@ -13,33 +13,71 @@ import { SearchResponse, AggregationsMultiBucketAggregateBase, AggregationsStringRareTermsBucketKeys, - Sort, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { buildDataTableRecord } from '@kbn/discover-utils'; +import { EsHitRecord } from '@kbn/discover-utils/types'; +import { MAX_FINDINGS_TO_LOAD } from '../../../common/constants'; import { CspVulnerabilityFinding } from '../../../../common/schemas'; -import { LATEST_VULNERABILITIES_INDEX_PATTERN } from '../../../../common/constants'; +import { + LATEST_VULNERABILITIES_INDEX_PATTERN, + LATEST_VULNERABILITIES_RETENTION_POLICY, +} from '../../../../common/constants'; import { useKibana } from '../../../common/hooks/use_kibana'; import { showErrorToast } from '../../../common/utils/show_error_toast'; import { FindingsBaseEsQuery } from '../../../common/types'; +import { VULNERABILITY_FIELDS } from '../constants'; +import { getCaseInsensitiveSortScript } from '../utils/custom_sort_script'; type LatestFindingsRequest = IKibanaSearchRequest; -type LatestFindingsResponse = IKibanaSearchResponse>; +type LatestFindingsResponse = IKibanaSearchResponse< + SearchResponse +>; interface FindingsAggs { count: AggregationsMultiBucketAggregateBase; } - interface VulnerabilitiesQuery extends FindingsBaseEsQuery { - sort: Sort; + sort: string[][]; enabled: boolean; - pageIndex: number; pageSize: number; } -export const getFindingsQuery = ({ query, sort, pageIndex, pageSize }: VulnerabilitiesQuery) => ({ +const getMultiFieldsSort = (sort: string[][]) => { + return sort.map(([id, direction]) => { + if (id === VULNERABILITY_FIELDS.PACKAGE_NAME) { + return getCaseInsensitiveSortScript(id, direction); + } + + return { + [id]: direction, + }; + }); +}; + +export const getVulnerabilitiesQuery = ( + { query, sort }: VulnerabilitiesQuery, + pageParam: number +) => ({ index: LATEST_VULNERABILITIES_INDEX_PATTERN, - query, - from: pageIndex * pageSize, - size: pageSize, - sort, + sort: getMultiFieldsSort(sort), + size: MAX_FINDINGS_TO_LOAD, + query: { + ...query, + bool: { + ...query?.bool, + filter: [ + ...(query?.bool?.filter ?? []), + { + range: { + '@timestamp': { + gte: `now-${LATEST_VULNERABILITIES_RETENTION_POLICY}`, + lte: 'now', + }, + }, + }, + ], + }, + }, + ...(pageParam ? { from: pageParam } : {}), }); export const useLatestVulnerabilities = (options: VulnerabilitiesQuery) => { @@ -47,19 +85,25 @@ export const useLatestVulnerabilities = (options: VulnerabilitiesQuery) => { data, notifications: { toasts }, } = useKibana().services; - return useQuery( + /** + * We're using useInfiniteQuery in this case to allow the user to fetch more data (if available and up to 10k) + * useInfiniteQuery differs from useQuery because it accumulates and caches a chunk of data from the previous fetches into an array + * it uses the getNextPageParam to know if there are more pages to load and retrieve the position of + * the last loaded record to be used as a from parameter to fetch the next chunk of data. + */ + return useInfiniteQuery( [LATEST_VULNERABILITIES_INDEX_PATTERN, options], - async () => { + async ({ pageParam }) => { const { rawResponse: { hits }, } = await lastValueFrom( data.search.search({ - params: getFindingsQuery(options), + params: getVulnerabilitiesQuery(options, pageParam), }) ); return { - page: hits.hits.map((hit) => hit._source!) as CspVulnerabilityFinding[], + page: hits.hits.map((hit) => buildDataTableRecord(hit as EsHitRecord)), total: number.is(hits.total) ? hits.total : 0, }; }, @@ -68,6 +112,12 @@ export const useLatestVulnerabilities = (options: VulnerabilitiesQuery) => { keepPreviousData: true, enabled: options.enabled, onError: (err: Error) => showErrorToast(toasts, err), + getNextPageParam: (lastPage, allPages) => { + if (lastPage.page.length < options.pageSize) { + return undefined; + } + return allPages.length * options.pageSize; + }, } ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx new file mode 100644 index 00000000000000..45fdc5c71a342c --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_grouping.tsx @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { getGroupingQuery } from '@kbn/securitysolution-grouping'; +import { + GroupingAggregation, + GroupPanelRenderer, + GroupStatsRenderer, + isNoneGroup, + NamedAggregation, + parseGroupingQuery, +} from '@kbn/securitysolution-grouping/src'; +import { useMemo } from 'react'; +import { LOCAL_STORAGE_VULNERABILITIES_GROUPING_KEY } from '../../../common/constants'; +import { useDataViewContext } from '../../../common/contexts/data_view_context'; +import { LATEST_VULNERABILITIES_RETENTION_POLICY } from '../../../../common/constants'; +import { + VulnerabilitiesGroupingAggregation, + VulnerabilitiesRootGroupingAggregation, + useGroupedVulnerabilities, +} from './use_grouped_vulnerabilities'; +import { + defaultGroupingOptions, + getDefaultQuery, + GROUPING_OPTIONS, + VULNERABILITY_FIELDS, +} from '../constants'; +import { useCloudSecurityGrouping } from '../../../components/cloud_security_grouping'; +import { VULNERABILITIES_UNIT, groupingTitle } from '../translations'; + +const getTermAggregation = (key: keyof VulnerabilitiesGroupingAggregation, field: string) => ({ + [key]: { + terms: { field, size: 1 }, + }, +}); + +const getAggregationsByGroupField = (field: string): NamedAggregation[] => { + if (isNoneGroup([field])) { + return []; + } + const aggMetrics: NamedAggregation[] = [ + { + groupByField: { + cardinality: { + field, + }, + }, + }, + ]; + + switch (field) { + case GROUPING_OPTIONS.RESOURCE_NAME: + return [...aggMetrics, getTermAggregation('resourceId', VULNERABILITY_FIELDS.RESOURCE_ID)]; + } + return aggMetrics; +}; + +/** + * Type Guard for checking if the given source is a VulnerabilitiesRootGroupingAggregation + */ +export const isVulnerabilitiesRootGroupingAggregation = ( + groupData: Record | undefined +): groupData is VulnerabilitiesRootGroupingAggregation => { + return groupData?.unitsCount?.value !== undefined; +}; + +/** + * Utility hook to get the latest vulnerabilities grouping data + * for the vulnerabilities page + */ +export const useLatestVulnerabilitiesGrouping = ({ + groupPanelRenderer, + groupStatsRenderer, +}: { + groupPanelRenderer?: GroupPanelRenderer; + groupStatsRenderer?: GroupStatsRenderer; +}) => { + const { dataView } = useDataViewContext(); + + const { + activePageIndex, + grouping, + pageSize, + query, + selectedGroup, + onChangeGroupsItemsPerPage, + onChangeGroupsPage, + setUrlQuery, + uniqueValue, + isNoneSelected, + onResetFilters, + error, + filters, + } = useCloudSecurityGrouping({ + dataView, + groupingTitle, + defaultGroupingOptions, + getDefaultQuery, + unit: VULNERABILITIES_UNIT, + groupPanelRenderer, + groupStatsRenderer, + groupingLocalStorageKey: LOCAL_STORAGE_VULNERABILITIES_GROUPING_KEY, + }); + + const groupingQuery = getGroupingQuery({ + additionalFilters: query ? [query] : [], + groupByField: selectedGroup, + uniqueValue, + from: `now-${LATEST_VULNERABILITIES_RETENTION_POLICY}`, + to: 'now', + pageNumber: activePageIndex * pageSize, + size: pageSize, + sort: [{ groupByField: { order: 'desc' } }], + statsAggregations: getAggregationsByGroupField(selectedGroup), + }); + + const { data, isFetching } = useGroupedVulnerabilities({ + query: groupingQuery, + enabled: !isNoneSelected, + }); + + const groupData = useMemo( + () => + parseGroupingQuery( + selectedGroup, + uniqueValue, + data as GroupingAggregation + ), + [data, selectedGroup, uniqueValue] + ); + + const isEmptyResults = + !isFetching && + isVulnerabilitiesRootGroupingAggregation(groupData) && + groupData.unitsCount?.value === 0; + + return { + groupData, + grouping, + isFetching, + activePageIndex, + pageSize, + selectedGroup, + onChangeGroupsItemsPerPage, + onChangeGroupsPage, + setUrlQuery, + isGroupSelected: !isNoneSelected, + isGroupLoading: !data, + onResetFilters, + filters, + error, + isEmptyResults, + }; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_table.tsx new file mode 100644 index 00000000000000..6c6f9cd112c57a --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_latest_vulnerabilities_table.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { Filter } from '@kbn/es-query'; +import { LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY } from '../../../common/constants'; +import { FindingsBaseURLQuery } from '../../../common/types'; +import { useCloudPostureDataTable } from '../../../common/hooks/use_cloud_posture_data_table'; +import { useLatestVulnerabilities } from './use_latest_vulnerabilities'; + +const columnsLocalStorageKey = 'cloudPosture:latestVulnerabilities:columns'; + +export const useLatestVulnerabilitiesTable = ({ + getDefaultQuery, + nonPersistedFilters, +}: { + getDefaultQuery: (params: FindingsBaseURLQuery) => FindingsBaseURLQuery; + nonPersistedFilters?: Filter[]; +}) => { + const cloudPostureDataTable = useCloudPostureDataTable({ + paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, + columnsLocalStorageKey, + defaultQuery: getDefaultQuery, + nonPersistedFilters, + }); + + const { query, sort, queryError, getRowsFromPages, pageSize } = cloudPostureDataTable; + + const { + data, + error: fetchError, + isFetching, + fetchNextPage, + } = useLatestVulnerabilities({ + query, + sort, + enabled: !queryError, + pageSize, + }); + + const rows = useMemo(() => getRowsFromPages(data?.pages), [data?.pages, getRowsFromPages]); + const total = data?.pages[0].total || 0; + + const error = fetchError || queryError; + + return { + cloudPostureDataTable, + rows, + error, + isFetching, + fetchNextPage, + total, + }; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_styles.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_styles.ts deleted file mode 100644 index c09490d719f1f8..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/hooks/use_styles.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEuiTheme } from '@elastic/eui'; -import { css, keyframes } from '@emotion/css'; - -export const useStyles = () => { - const { euiTheme } = useEuiTheme(); - - const highlight = keyframes` - 0% { background-color: ${euiTheme.colors.warning};} - 50% { background-color: ${euiTheme.colors.emptyShade};} - 75% { background-color: ${euiTheme.colors.warning};} - 100% { background-color: ${euiTheme.colors.emptyShade};} - `; - - const gridStyle = css` - & .euiDataGrid__content { - background: transparent; - } - & .euiDataGridHeaderCell__icon { - display: none; - } - & .euiDataGrid__controls { - border-bottom: none; - margin-bottom: ${euiTheme.size.s}; - - & .euiButtonEmpty { - font-weight: ${euiTheme.font.weight.bold}; - } - } - & .euiDataGrid__leftControls { - > .euiButtonEmpty:hover:not(:disabled), - .euiButtonEmpty:focus { - text-decoration: none; - cursor: default; - } - } - & .euiButtonIcon { - color: ${euiTheme.colors.primary}; - } - & .euiDataGridRowCell { - font-size: ${euiTheme.size.m}; - - // Vertically center content - .euiDataGridRowCell__content { - display: flex; - align-items: center; - } - } - /* EUI QUESTION: Why is this being done via CSS instead of setting isExpandable: false in the columns API? */ - & .euiDataGridRowCell__actions > .euiDataGridRowCell__expandCell { - display: none; - } - & .euiDataGridRowCell.euiDataGridRowCell--numeric { - text-align: left; - } - & .euiDataGridHeaderCell--numeric .euiDataGridHeaderCell__content { - flex-grow: 0; - text-align: left; - } - `; - - const highlightStyle = css` - & [data-test-subj='dataGridColumnSortingButton'] .euiButtonEmpty__text { - animation: ${highlight} 1s ease-out infinite; - color: ${euiTheme.colors.darkestShade}; - } - `; - - const groupBySelector = css` - width: 188px; - display: inline-block; - margin-left: 8px; - `; - - return { - highlightStyle, - gridStyle, - groupBySelector, - }; -}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_container.tsx new file mode 100644 index 00000000000000..8ba50c3aac4f13 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_container.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Filter } from '@kbn/es-query'; +import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { useLatestVulnerabilitiesGrouping } from './hooks/use_latest_vulnerabilities_grouping'; +import { LatestVulnerabilitiesTable } from './latest_vulnerabilities_table'; +import { groupPanelRenderer, groupStatsRenderer } from './latest_vulnerabilities_group_renderer'; +import { FindingsSearchBar } from '../configurations/layout/findings_search_bar'; +import { ErrorCallout } from '../configurations/layout/error_callout'; +import { EmptyState } from '../../components/empty_state'; +import { CloudSecurityGrouping } from '../../components/cloud_security_grouping'; +import { DEFAULT_GROUPING_TABLE_HEIGHT } from '../../common/constants'; + +export const LatestVulnerabilitiesContainer = () => { + const renderChildComponent = (groupFilters: Filter[]) => { + return ( + + ); + }; + + const { + isGroupSelected, + groupData, + grouping, + isFetching, + activePageIndex, + pageSize, + selectedGroup, + onChangeGroupsItemsPerPage, + onChangeGroupsPage, + setUrlQuery, + isGroupLoading, + onResetFilters, + error, + isEmptyResults, + } = useLatestVulnerabilitiesGrouping({ groupPanelRenderer, groupStatsRenderer }); + + if (error || isEmptyResults) { + return ( + <> + + + {error && } + {isEmptyResults && } + + ); + } + if (isGroupSelected) { + return ( + <> + +
+ + +
+ + ); + } + + return ( + <> + + + + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_group_renderer.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_group_renderer.tsx new file mode 100644 index 00000000000000..82626fd6845131 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_group_renderer.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiTextBlockTruncate, + EuiToolTip, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { GroupPanelRenderer, RawBucket, StatRenderer } from '@kbn/securitysolution-grouping/src'; +import React from 'react'; +import { VulnerabilitiesGroupingAggregation } from './hooks/use_grouped_vulnerabilities'; +import { GROUPING_OPTIONS } from './constants'; +import { VULNERABILITIES_GROUPING_COUNTER } from './test_subjects'; +import { NULL_GROUPING_MESSAGES, NULL_GROUPING_UNIT, VULNERABILITIES } from './translations'; +import { getAbbreviatedNumber } from '../../common/utils/get_abbreviated_number'; +import { LoadingGroup, NullGroup } from '../../components/cloud_security_grouping'; + +export const groupPanelRenderer: GroupPanelRenderer = ( + selectedGroup, + bucket, + nullGroupMessage, + isLoading +) => { + if (isLoading) { + return ; + } + + const renderNullGroup = (title: string) => ( + + ); + + switch (selectedGroup) { + case GROUPING_OPTIONS.RESOURCE_NAME: + return nullGroupMessage ? ( + renderNullGroup(NULL_GROUPING_MESSAGES.RESOURCE_NAME) + ) : ( + + + + + + + {bucket.key_as_string} {bucket.resourceId?.buckets?.[0].key} + + + + + + + ); + default: + return nullGroupMessage ? ( + renderNullGroup(NULL_GROUPING_MESSAGES.DEFAULT) + ) : ( + + + + + + {bucket.key_as_string} + + + + + + ); + } +}; + +const VulnerabilitiesCountComponent = ({ + bucket, +}: { + bucket: RawBucket; +}) => { + const { euiTheme } = useEuiTheme(); + + return ( + + + {getAbbreviatedNumber(bucket.doc_count)} + + + ); +}; + +const VulnerabilitiesCount = React.memo(VulnerabilitiesCountComponent); + +export const groupStatsRenderer = ( + selectedGroup: string, + bucket: RawBucket +): StatRenderer[] => { + const defaultBadges = [ + { + title: VULNERABILITIES, + renderer: , + }, + ]; + + return defaultBadges; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_table.tsx new file mode 100644 index 00000000000000..b27ebfb459fe45 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/latest_vulnerabilities_table.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { DataTableRecord } from '@kbn/discover-utils/types'; +import { i18n } from '@kbn/i18n'; +import { EuiDataGridCellValueElementProps, EuiSpacer } from '@elastic/eui'; +import { Filter } from '@kbn/es-query'; +import { CspVulnerabilityFinding } from '../../../common/schemas'; +import { CloudSecurityDataTable } from '../../components/cloud_security_data_table'; +import { useLatestVulnerabilitiesTable } from './hooks/use_latest_vulnerabilities_table'; +import { LATEST_VULNERABILITIES_TABLE } from './test_subjects'; +import { getDefaultQuery, defaultColumns } from './constants'; +import { VulnerabilityFindingFlyout } from './vulnerabilities_finding_flyout/vulnerability_finding_flyout'; +import { ErrorCallout } from '../configurations/layout/error_callout'; +import { CVSScoreBadge, SeverityStatusBadge } from '../../components/vulnerability_badges'; + +interface LatestVulnerabilitiesTableProps { + groupSelectorComponent?: JSX.Element; + height?: number; + nonPersistedFilters?: Filter[]; +} +/** + * Type Guard for checking if the given source is a CspVulnerabilityFinding + */ +const isCspVulnerabilityFinding = ( + source: Record | undefined +): source is CspVulnerabilityFinding => { + return source?.vulnerability?.id !== undefined; +}; + +/** + * This Wrapper component renders the children if the given row is a CspVulnerabilityFinding + * it uses React's Render Props pattern + */ +const CspVulnerabilityFindingRenderer = ({ + row, + children, +}: { + row: DataTableRecord; + children: ({ finding }: { finding: CspVulnerabilityFinding }) => JSX.Element; +}) => { + const source = row.raw._source; + const finding = isCspVulnerabilityFinding(source) && (source as CspVulnerabilityFinding); + if (!finding) return <>; + return children({ finding }); +}; + +const flyoutComponent = (row: DataTableRecord, onCloseFlyout: () => void): JSX.Element => { + return ( + + {({ finding }) => ( + + )} + + ); +}; + +const title = i18n.translate('xpack.csp.findings.latestVulnerabilities.tableRowTypeLabel', { + defaultMessage: 'Vulnerabilities', +}); + +const customCellRenderer = (rows: DataTableRecord[]) => ({ + 'vulnerability.score.base': ({ rowIndex }: EuiDataGridCellValueElementProps) => ( + + {({ finding }) => ( + + )} + + ), + 'vulnerability.severity': ({ rowIndex }: EuiDataGridCellValueElementProps) => ( + + {({ finding }) => } + + ), +}); + +export const LatestVulnerabilitiesTable = ({ + groupSelectorComponent, + height, + nonPersistedFilters, +}: LatestVulnerabilitiesTableProps) => { + const { cloudPostureDataTable, rows, total, error, isFetching, fetchNextPage } = + useLatestVulnerabilitiesTable({ + getDefaultQuery, + nonPersistedFilters, + }); + + const { filters } = cloudPostureDataTable; + + return ( + <> + {error ? ( + <> + + + + ) : ( + 0 ? 404 : 364}px)`} + /> + )} + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/test_subjects.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/test_subjects.ts index 72211cc7784316..8ad512f8a41eea 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/test_subjects.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/test_subjects.ts @@ -13,3 +13,7 @@ export const OVERVIEW_TAB_VULNERABILITY_FLYOUT = 'vulnerability_overview_tab_fly export const SEVERITY_STATUS_VULNERABILITY_FLYOUT = 'vulnerability_severity_status_flyout'; export const TAB_ID_VULNERABILITY_FLYOUT = (tabId: string) => `vulnerability-finding-flyout-tab-${tabId}`; + +export const LATEST_VULNERABILITIES_TABLE = 'latest_vulnerabilities_table'; + +export const VULNERABILITIES_GROUPING_COUNTER = 'vulnerabilities_grouping_counter'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/translations.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/translations.ts index b2c0ca0ca83663..65ca61056f612b 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/translations.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/translations.ts @@ -22,3 +22,35 @@ export const SEARCH_BAR_PLACEHOLDER = i18n.translate( export const VULNERABILITIES = i18n.translate('xpack.csp.vulnerabilities', { defaultMessage: 'Vulnerabilities', }); + +export const VULNERABILITIES_UNIT = (totalCount: number) => + i18n.translate('xpack.csp.vulnerabilities.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {vulnerability} other {vulnerabilities}}`, + }); + +export const NULL_GROUPING_UNIT = i18n.translate( + 'xpack.csp.vulnerabilities.grouping.nullGroupUnit', + { + defaultMessage: 'vulnerabilities', + } +); + +export const NULL_GROUPING_MESSAGES = { + RESOURCE_NAME: i18n.translate('xpack.csp.vulnerabilities.grouping.resource.nullGroupTitle', { + defaultMessage: 'No resource', + }), + DEFAULT: i18n.translate('xpack.csp.vulnerabilities.grouping.default.nullGroupTitle', { + defaultMessage: 'No grouping', + }), +}; + +export const GROUPING_LABELS = { + RESOURCE_NAME: i18n.translate('xpack.csp.findings.latestFindings.groupByResource', { + defaultMessage: 'Resource', + }), +}; + +export const groupingTitle = i18n.translate('xpack.csp.vulnerabilities.latestFindings.groupBy', { + defaultMessage: 'Group vulnerabilities by', +}); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_filters.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_filters.ts deleted file mode 100644 index 7f7d9ff544c62d..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_filters.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - type Filter, - buildFilter, - FILTERS, - FilterStateStore, - compareFilters, - FilterCompareOptions, -} from '@kbn/es-query'; -import type { Serializable } from '@kbn/utility-types'; -import type { FindingsBaseProps } from '../../../common/types'; - -const compareOptions: FilterCompareOptions = { - negate: false, -}; - -/** - * adds a new filter to a new filters array - * removes existing filter if negated filter is added - * - * @returns {Filter[]} a new array of filters to be added back to filterManager - */ -export const getFilters = ({ - filters: existingFilters, - dataView, - field, - value, - negate, -}: { - filters: Filter[]; - dataView: FindingsBaseProps['dataView']; - field: string; - value: Serializable; - negate: boolean; -}): Filter[] => { - const dataViewField = dataView.fields.find((f) => f.spec.name === field); - if (!dataViewField) return existingFilters; - - const phraseFilter = buildFilter( - dataView, - dataViewField, - FILTERS.PHRASE, - negate, - false, - value, - null, - FilterStateStore.APP_STATE - ); - - const nextFilters = [ - ...existingFilters.filter( - // Exclude existing filters that match the newly added 'phraseFilter' - (filter) => !compareFilters(filter, phraseFilter, compareOptions) - ), - phraseFilter, - ]; - - return nextFilters; -}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_vulnerabilities_grid_cell_actions.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_vulnerabilities_grid_cell_actions.test.tsx deleted file mode 100644 index 1f84f294d8be1f..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_vulnerabilities_grid_cell_actions.test.tsx +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getRowValueByColumnId } from './get_vulnerabilities_grid_cell_actions'; -import { vulnerabilitiesColumns } from '../vulnerabilities_table_columns'; -import { vulnerabilitiesByResourceColumns } from '../vulnerabilities_by_resource/vulnerabilities_by_resource_table_columns'; -import { CspVulnerabilityFinding } from '../../../../common/schemas'; - -describe('getRowValueByColumnId', () => { - it('should return vulnerability id', () => { - const vulnerabilityRow = { - vulnerability: { - id: 'CVE-2017-1000117', - }, - }; - const columns = vulnerabilitiesColumns; - const columnId = columns.vulnerability; - - expect( - getRowValueByColumnId(vulnerabilityRow as Partial, columns, columnId) - ).toEqual('CVE-2017-1000117'); - }); - - it('should return base as a vulnerability score', () => { - const vulnerabilityRow = { - vulnerability: { - score: { - base: 5, - version: 'v1', - }, - }, - }; - const columns = vulnerabilitiesColumns; - const columnId = columns.cvss; - - expect( - getRowValueByColumnId(vulnerabilityRow as Partial, columns, columnId) - ).toEqual(5); - }); - - it('should return undefined when no base score is available', () => { - const vulnerabilityRow = { - vulnerability: {}, - }; - const columns = vulnerabilitiesColumns; - const columnId = columns.cvss; - - expect( - getRowValueByColumnId(vulnerabilityRow as Partial, columns, columnId) - ).toEqual(undefined); - - const vulnerabilityRow2 = { - vulnerability: { - score: { - version: 'v1', - }, - }, - }; - - expect( - getRowValueByColumnId( - vulnerabilityRow2 as Partial, - columns, - columnId - ) - ).toEqual(undefined); - }); - - it('should return resource id', () => { - const vulnerabilityRow = { - resource: { - id: 'i-1234567890abcdef0', - }, - }; - const columns = vulnerabilitiesByResourceColumns; - const columnId = columns.resourceId; - - expect( - getRowValueByColumnId(vulnerabilityRow as Partial, columns, columnId) - ).toEqual('i-1234567890abcdef0'); - }); - - it('should return resource name', () => { - const vulnerabilityRow = { - resource: { - name: 'test', - }, - }; - const columns1 = vulnerabilitiesByResourceColumns; - const columns2 = vulnerabilitiesColumns; - const columnId1 = columns1.resourceName; - const columnId2 = columns2.resourceName; - - expect( - getRowValueByColumnId( - vulnerabilityRow as Partial, - columns1, - columnId1 - ) - ).toEqual('test'); - expect( - getRowValueByColumnId( - vulnerabilityRow as Partial, - columns2, - columnId2 - ) - ).toEqual('test'); - }); - - it('should return vulnerability severity', () => { - const vulnerabilityRow = { - vulnerability: { - severity: 'high', - }, - }; - const columns = vulnerabilitiesColumns; - const columnId = columns.severity; - - expect( - getRowValueByColumnId(vulnerabilityRow as Partial, columns, columnId) - ).toEqual('high'); - }); - - it('should return package fields', () => { - const vulnerabilityRow = { - package: { - name: 'test', - version: '1.0.0', - fixed_version: '1.0.1', - }, - }; - const columns1 = vulnerabilitiesColumns; - const columnId1 = columns1.package; - const columnId2 = columns1.version; - const columnId3 = columns1.fixedVersion; - - expect( - getRowValueByColumnId( - vulnerabilityRow as Partial, - columns1, - columnId1 - ) - ).toEqual('test'); - expect( - getRowValueByColumnId( - vulnerabilityRow as Partial, - columns1, - columnId2 - ) - ).toEqual('1.0.0'); - expect( - getRowValueByColumnId( - vulnerabilityRow as Partial, - columns1, - columnId3 - ) - ).toEqual('1.0.1'); - }); - - it('should return undefined is package is missing', () => { - const vulnerabilityRow = { - vulnerability: {}, - }; - const columns = vulnerabilitiesColumns; - const columnId = columns.package; - - expect( - getRowValueByColumnId(vulnerabilityRow as Partial, columns, columnId) - ).toEqual(undefined); - }); - - it('should return cloud region', () => { - const vulnerabilityRow = { - cloud: { - region: 'us-east-1', - }, - }; - const columns = vulnerabilitiesByResourceColumns; - const columnId = columns.region; - - expect( - getRowValueByColumnId(vulnerabilityRow as Partial, columns, columnId) - ).toEqual('us-east-1'); - }); -}); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_vulnerabilities_grid_cell_actions.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_vulnerabilities_grid_cell_actions.tsx deleted file mode 100644 index dbde094d6f43bf..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_vulnerabilities_grid_cell_actions.tsx +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { EuiDataGridColumn, EuiDataGridColumnCellAction, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { CspVulnerabilityFinding } from '../../../../common/schemas'; -import { getFilters } from './get_filters'; -import { FILTER_IN, FILTER_OUT } from '../translations'; - -export const getRowValueByColumnId = ( - vulnerabilityRow: Partial, - columns: Record, - columnId: string -) => { - if (columnId === columns.vulnerability) { - return vulnerabilityRow.vulnerability?.id; - } - if (columnId === columns.cvss) { - return vulnerabilityRow.vulnerability?.score?.base; - } - if (columnId === columns.resourceId) { - return vulnerabilityRow.resource?.id; - } - if (columnId === columns.resourceName) { - return vulnerabilityRow.resource?.name; - } - if (columnId === columns.severity) { - return vulnerabilityRow.vulnerability?.severity; - } - if (columnId === columns.package) { - return vulnerabilityRow.package?.name; - } - if (columnId === columns.version) { - return vulnerabilityRow.package?.version; - } - if (columnId === columns.fixedVersion) { - return vulnerabilityRow.package?.fixed_version; - } - if (columnId === columns.region) { - return vulnerabilityRow.cloud?.region; - } -}; - -export const getVulnerabilitiesGridCellActions = < - T extends Array> ->({ - data, - columns, - columnGridFn, - pageSize, - setUrlQuery, - filters, - dataView, -}: { - data: T; - columns: Record; - columnGridFn: (cellActions: EuiDataGridColumnCellAction[]) => EuiDataGridColumn[]; - pageSize: number; - setUrlQuery: (query: any) => void; - filters: any; - dataView: any; -}) => { - const getColumnIdValue = (rowIndex: number, columnId: string) => { - const vulnerabilityRow = data[rowIndex]; - if (!vulnerabilityRow) return null; - - return getRowValueByColumnId(vulnerabilityRow, columns, columnId); - }; - - const cellActions: EuiDataGridColumnCellAction[] = [ - ({ Component, rowIndex, columnId }) => { - const rowIndexFromPage = rowIndex > pageSize - 1 ? rowIndex % pageSize : rowIndex; - - const value = getColumnIdValue(rowIndexFromPage, columnId); - - if (!value) return null; - return ( - - { - setUrlQuery({ - pageIndex: 0, - filters: getFilters({ - filters, - dataView, - field: columnId, - value, - negate: false, - }), - }); - }} - > - {FILTER_IN} - - - ); - }, - ({ Component, rowIndex, columnId }) => { - const rowIndexFromPage = rowIndex > pageSize - 1 ? rowIndex % pageSize : rowIndex; - - const value = getColumnIdValue(rowIndexFromPage, columnId); - - if (!value) return null; - return ( - - { - setUrlQuery({ - pageIndex: 0, - filters: getFilters({ - filters, - dataView, - field: columnId, - value, - negate: true, - }), - }); - }} - > - {FILTER_OUT} - - - ); - }, - ]; - - return columnGridFn(cellActions); -}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx index 9c74d7640beac4..aca54e19bfccf0 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx @@ -4,486 +4,42 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { - EuiButtonEmpty, - EuiButtonIcon, - EuiDataGrid, - EuiDataGridCellValueElementProps, - EuiFlexItem, - EuiProgress, - EuiSpacer, - useEuiTheme, -} from '@elastic/eui'; -import { cx } from '@emotion/css'; -import { DataView } from '@kbn/data-views-plugin/common'; -import React, { useCallback, useMemo, useState, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; +import React from 'react'; import { Routes, Route } from '@kbn/shared-ux-router'; -import { LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY } from '../../common/constants'; -import { - CloudPostureTableResult, - useCloudPostureTable, -} from '../../common/hooks/use_cloud_posture_table'; -import { useLatestVulnerabilities } from './hooks/use_latest_vulnerabilities'; -import type { 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'; -import { CVSScoreBadge, SeverityStatusBadge } from '../../components/vulnerability_badges'; -import { EmptyState } from '../../components/empty_state'; -import { VulnerabilityFindingFlyout } from './vulnerabilities_finding_flyout/vulnerability_finding_flyout'; import { NoVulnerabilitiesStates } from '../../components/no_vulnerabilities_states'; import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api'; -import { useLimitProperties } from '../../common/utils/get_limit_properties'; -import { LimitedResultsBar } from '../configurations/layout/findings_layout'; -import { - getVulnerabilitiesColumnsGrid, - vulnerabilitiesColumns, -} from './vulnerabilities_table_columns'; -import { defaultLoadingRenderer, defaultNoDataRenderer } from '../../components/cloud_posture_page'; -import { SEARCH_BAR_PLACEHOLDER, VULNERABILITIES } from './translations'; -import { - severitySchemaConfig, - severitySortScript, - getCaseInsensitiveSortScript, -} from './utils/custom_sort_script'; -import { useStyles } from './hooks/use_styles'; -import { FindingsGroupBySelector } from '../configurations/layout/findings_group_by_selector'; -import { vulnerabilitiesPathnameHandler } from './utils/vulnerabilities_pathname_handler'; +import { CloudPosturePage } from '../../components/cloud_posture_page'; import { findingsNavigation } from '../../common/navigation/constants'; -import { VulnerabilitiesByResource } from './vulnerabilities_by_resource/vulnerabilities_by_resource'; -import { ResourceVulnerabilities } from './vulnerabilities_by_resource/resource_vulnerabilities/resource_vulnerabilities'; -import { getVulnerabilitiesGridCellActions } from './utils/get_vulnerabilities_grid_cell_actions'; import { useLatestFindingsDataView } from '../../common/api/use_latest_findings_data_view'; - -const getDefaultQuery = ({ query, filters }: any): any => ({ - query, - filters, - sort: [ - { id: vulnerabilitiesColumns.severity, direction: 'desc' }, - { id: vulnerabilitiesColumns.cvss, direction: 'desc' }, - ], - pageIndex: 0, -}); - -const VulnerabilitiesDataGrid = ({ - dataView, - data, - isFetching, - onChangeItemsPerPage, - onChangePage, - onSort, - urlQuery, - onResetFilters, - pageSize, - setUrlQuery, - pageIndex, - sort, -}: { - dataView: DataView; - data: VulnerabilitiesQueryData | undefined; - isFetching: boolean; -} & Pick< - CloudPostureTableResult, - | 'pageIndex' - | 'sort' - | 'pageSize' - | 'onChangeItemsPerPage' - | 'onChangePage' - | 'onSort' - | 'urlQuery' - | 'setUrlQuery' - | 'onResetFilters' ->) => { - const { euiTheme } = useEuiTheme(); - const styles = useStyles(); - const [showHighlight, setHighlight] = useState(false); - - const invalidIndex = -1; - - const selectedVulnerability = useMemo(() => { - if (urlQuery.vulnerabilityIndex !== undefined) { - return data?.page[urlQuery.vulnerabilityIndex]; - } - }, [data?.page, urlQuery.vulnerabilityIndex]); - - const onCloseFlyout = () => { - setUrlQuery({ - vulnerabilityIndex: invalidIndex, - }); - }; - - const onSortHandler = useCallback( - (newSort: any) => { - onSort(newSort); - if (newSort.length !== sort.length) { - setHighlight(true); - setTimeout(() => { - setHighlight(false); - }, 2000); - } - }, - [onSort, sort] - ); - - const { isLastLimitedPage, limitedTotalItemCount } = useLimitProperties({ - total: data?.total, - pageIndex, - pageSize, - }); - - const onOpenFlyout = useCallback( - (vulnerabilityRow: VulnerabilitiesQueryData['page'][number]) => { - const vulnerabilityIndex = data?.page.findIndex( - (vulnerabilityRecord: VulnerabilitiesQueryData['page'][number]) => - vulnerabilityRecord.vulnerability?.id === vulnerabilityRow.vulnerability?.id && - vulnerabilityRecord.resource?.id === vulnerabilityRow.resource?.id && - vulnerabilityRecord.package.name === vulnerabilityRow.package.name && - vulnerabilityRecord.package.version === vulnerabilityRow.package.version - ); - setUrlQuery({ - vulnerabilityIndex, - }); - }, - [setUrlQuery, data?.page] - ); - - const columns = useMemo(() => { - if (!data?.page) { - return []; - } - return getVulnerabilitiesGridCellActions({ - columnGridFn: getVulnerabilitiesColumnsGrid, - columns: vulnerabilitiesColumns, - dataView, - pageSize, - data: data.page, - setUrlQuery, - filters: urlQuery.filters, - }); - }, [data?.page, dataView, pageSize, setUrlQuery, urlQuery.filters]); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState( - columns.map(({ id }) => id) // initialize to the full set of columns - ); - - const flyoutVulnerabilityIndex = urlQuery?.vulnerabilityIndex; - - const selectedVulnerabilityIndex = flyoutVulnerabilityIndex - ? flyoutVulnerabilityIndex + pageIndex * pageSize - : undefined; - - const renderCellValue = useMemo(() => { - const Cell: React.FC = ({ - columnId, - rowIndex, - setCellProps, - }): React.ReactElement | null => { - const rowIndexFromPage = rowIndex > pageSize - 1 ? rowIndex % pageSize : rowIndex; - - const vulnerabilityRow = data?.page[rowIndexFromPage]; - - useEffect(() => { - if (selectedVulnerabilityIndex === rowIndex) { - setCellProps({ - style: { - backgroundColor: euiTheme.colors.highlight, - }, - }); - } else { - setCellProps({ - style: { - backgroundColor: 'inherit', - }, - }); - } - }, [rowIndex, setCellProps]); - - if (isFetching) return null; - if (!vulnerabilityRow) return null; - if (!vulnerabilityRow.vulnerability?.id) return null; - - if (columnId === vulnerabilitiesColumns.actions) { - return ( - { - onOpenFlyout(vulnerabilityRow); - }} - /> - ); - } - if (columnId === vulnerabilitiesColumns.vulnerability) { - return <>{vulnerabilityRow.vulnerability?.id}; - } - if (columnId === vulnerabilitiesColumns.cvss) { - if ( - !vulnerabilityRow.vulnerability.score?.base || - !vulnerabilityRow.vulnerability.score?.version - ) { - return null; - } - return ( - - ); - } - if (columnId === vulnerabilitiesColumns.resourceName) { - return <>{vulnerabilityRow.resource?.name}; - } - if (columnId === vulnerabilitiesColumns.resourceId) { - return <>{vulnerabilityRow.resource?.id}; - } - if (columnId === vulnerabilitiesColumns.severity) { - if (!vulnerabilityRow.vulnerability.severity) { - return null; - } - return ; - } - - if (columnId === vulnerabilitiesColumns.package) { - return <>{vulnerabilityRow?.package?.name}; - } - if (columnId === vulnerabilitiesColumns.version) { - return <>{vulnerabilityRow?.package?.version}; - } - if (columnId === vulnerabilitiesColumns.fixedVersion) { - return <>{vulnerabilityRow?.package?.fixed_version}; - } - - return null; - }; - - return Cell; - }, [ - data?.page, - euiTheme.colors.highlight, - onOpenFlyout, - pageSize, - selectedVulnerabilityIndex, - isFetching, - ]); - - const showVulnerabilityFlyout = flyoutVulnerabilityIndex > invalidIndex; - - if (data?.page.length === 0) { - return ; - } - - const dataTableStyle = { - // Change the height of the grid to fit the page - // If there are filters, leave space for the filter bar - // Todo: Replace this component with EuiAutoSizer - height: `calc(100vh - ${urlQuery.filters.length > 0 ? 403 : 363}px)`, - minHeight: 400, - }; - - return ( - <> - -
- - - {i18n.translate('xpack.csp.vulnerabilities.totalVulnerabilities', { - defaultMessage: - '{total, plural, one {# Vulnerability} other {# Vulnerabilities}}', - values: { total: data?.total }, - })} - - - ), - }, - right: ( - - - - ), - }, - }} - 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, - }} - virtualizationOptions={{ - overscanRowCount: 20, - }} - /> - {isLastLimitedPage && } -
- {showVulnerabilityFlyout && selectedVulnerability && ( - - )} - - ); -}; - -const VulnerabilitiesContent = ({ dataView }: { dataView: DataView }) => { - const { - sort, - query, - queryError, - pageSize, - pageIndex, - onChangeItemsPerPage, - onChangePage, - onSort, - urlQuery, - setUrlQuery, - onResetFilters, - } = 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 (isLoading && !error) { - return defaultLoadingRenderer(); - } - - if (!data?.page && !error) { - return defaultNoDataRenderer(); - } - - return ( - <> - { - setUrlQuery({ ...newQuery, pageIndex: 0 }); - }} - loading={isFetching} - placeholder={SEARCH_BAR_PLACEHOLDER} - /> - - {error && } - {!error && ( - - )} - - ); -}; +import { LatestVulnerabilitiesContainer } from './latest_vulnerabilities_container'; +import { DataViewContext } from '../../common/contexts/data_view_context'; export const Vulnerabilities = () => { - const { data, isLoading, error } = useLatestFindingsDataView( - LATEST_VULNERABILITIES_INDEX_PATTERN - ); + const dataViewQuery = useLatestFindingsDataView(LATEST_VULNERABILITIES_INDEX_PATTERN); const getSetupStatus = useCspSetupStatusApi(); if (getSetupStatus?.data?.vuln_mgmt?.status !== 'indexed') return ; - if (error) { - return ; - } - if (isLoading) { - return defaultLoadingRenderer(); - } - - if (!data) { - return defaultNoDataRenderer(); - } + const dataViewContextValue = { + dataView: dataViewQuery.data!, + dataViewRefetch: dataViewQuery.refetch, + dataViewIsRefetching: dataViewQuery.isRefetching, + }; return ( - - } - /> - } - /> - } - /> - + + + ( + + + + )} + /> + + ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/__mocks__/vulnerabilities_by_resource.mock.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/__mocks__/vulnerabilities_by_resource.mock.ts deleted file mode 100644 index 4ad13e2d215a1e..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/__mocks__/vulnerabilities_by_resource.mock.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const getVulnerabilitiesByResourceData = () => ({ - total: 2, - total_vulnerabilities: 8, - page: [ - { - resource: { id: 'resource-id-1', name: 'resource-test-1' }, - cloud: { region: 'us-test-1' }, - vulnerabilities_count: 4, - severity_map: { - critical: 1, - high: 1, - medium: 1, - low: 1, - }, - }, - { - resource: { id: 'resource-id-2', name: 'resource-test-2' }, - cloud: { region: 'us-test-1' }, - vulnerabilities_count: 4, - severity_map: { - critical: 1, - high: 1, - medium: 1, - low: 1, - }, - }, - ], -}); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/__mocks__/resource_vulnerabilities.mock.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/__mocks__/resource_vulnerabilities.mock.ts deleted file mode 100644 index 8328192062cc04..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/__mocks__/resource_vulnerabilities.mock.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const getResourceVulnerabilitiesMockData = () => ({ - page: [ - { - agent: { - name: 'ip-172-31-15-210', - id: '2d262db5-b637-4e46-a2a0-db409825ff46', - ephemeral_id: '2af1be77-0bdf-4313-b375-592848fe60d7', - type: 'cloudbeat', - version: '8.8.0', - }, - package: { - path: 'usr/lib/snapd/snapd', - fixed_version: '3.0.0-20220521103104-8f96da9f5d5e', - name: 'gopkg.in/yaml.v3', - type: 'gobinary', - version: 'v3.0.0-20210107192922-496545a6307b', - }, - resource: { - name: 'elastic-agent-instance-a6c683d0-0977-11ee-bb0b-0af2059ffbbf', - id: '0d103e99f17f355ba', - }, - elastic_agent: { - id: '2d262db5-b637-4e46-a2a0-db409825ff46', - version: '8.8.0', - snapshot: false, - }, - vulnerability: { - severity: 'HIGH', - package: { - fixed_version: '3.0.0-20220521103104-8f96da9f5d5e', - name: 'gopkg.in/yaml.v3', - version: 'v3.0.0-20210107192922-496545a6307b', - }, - description: - 'An issue in the Unmarshal function in Go-Yaml v3 causes the program to crash when attempting to deserialize invalid input.', - title: 'crash when attempting to deserialize invalid input', - classification: 'CVSS', - data_source: { - ID: 'go-vulndb', - URL: 'https://github.com/golang/vulndb', - Name: 'The Go Vulnerability Database', - }, - cwe: ['CWE-502'], - reference: 'https://avd.aquasec.com/nvd/cve-2022-28948', - score: { - version: '3.1', - base: 7.5, - }, - report_id: 1686633719, - scanner: { - vendor: 'Trivy', - version: 'v0.35.0', - }, - id: 'CVE-2022-28948', - enumeration: 'CVE', - published_date: '2022-05-19T20:15:00Z', - class: 'lang-pkgs', - cvss: { - redhat: { - V3Vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H', - V3Score: 7.5, - }, - nvd: { - V3Vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H', - V2Vector: 'AV:N/AC:L/Au:N/C:N/I:N/A:P', - V3Score: 7.5, - V2Score: 5, - }, - ghsa: { - V3Vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H', - V3Score: 7.5, - }, - }, - }, - cloud: { - provider: 'aws', - region: 'us-east-1', - account: { - name: 'elastic-security-cloud-security-dev', - id: '704479110758', - }, - }, - '@timestamp': '2023-06-13T06:15:16.182Z', - cloudbeat: { - commit_sha: '8497f3a4b4744c645233c5a13b45400367411c2f', - commit_time: '2023-05-09T16:07:58Z', - version: '8.8.0', - }, - ecs: { - version: '8.6.0', - }, - data_stream: { - namespace: 'default', - type: 'logs', - dataset: 'cloud_security_posture.vulnerabilities', - }, - host: { - name: 'ip-172-31-15-210', - }, - event: { - agent_id_status: 'auth_metadata_missing', - sequence: 1686633719, - ingested: '2023-06-15T18:37:56Z', - created: '2023-06-13T06:15:16.18250081Z', - kind: 'state', - id: '5cad2983-4a74-455d-ab39-6c584acd3994', - type: ['info'], - category: ['vulnerability'], - dataset: 'cloud_security_posture.vulnerabilities', - outcome: 'success', - }, - }, - ], - total: 1, -}); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/resource_vulnerabilities.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/resource_vulnerabilities.test.tsx deleted file mode 100644 index 9de1a61bebe38a..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/resource_vulnerabilities.test.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { useParams } from 'react-router-dom'; -import { ResourceVulnerabilities } from './resource_vulnerabilities'; -import { TestProvider } from '../../../../test/test_provider'; -import { useLatestVulnerabilities } from '../../hooks/use_latest_vulnerabilities'; -import { getResourceVulnerabilitiesMockData } from './__mocks__/resource_vulnerabilities.mock'; -import { VULNERABILITIES_CVSS_SCORE_BADGE_SUBJ } from '../../../../components/test_subjects'; - -jest.mock('../../hooks/use_latest_vulnerabilities', () => ({ - useLatestVulnerabilities: jest.fn(), -})); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: jest.fn().mockReturnValue({ - integration: undefined, - }), -})); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe('ResourceVulnerabilities', () => { - const dataView: any = {}; - - const renderVulnerabilityByResource = () => { - return render( - - - - ); - }; - - it('renders the loading state', () => { - (useLatestVulnerabilities as jest.Mock).mockReturnValue({ - data: undefined, - isLoading: true, - isFetching: true, - }); - renderVulnerabilityByResource(); - expect(screen.getByText(/loading/i)).toBeInTheDocument(); - }); - it('renders the no data state', () => { - (useLatestVulnerabilities as jest.Mock).mockReturnValue({ - data: undefined, - isLoading: false, - isFetching: false, - }); - - renderVulnerabilityByResource(); - expect(screen.getByText(/no data/i)).toBeInTheDocument(); - }); - - it('applies the correct filter on fetch', () => { - const resourceId = 'test'; - (useParams as jest.Mock).mockReturnValue({ - resourceId, - }); - renderVulnerabilityByResource(); - expect(useLatestVulnerabilities).toHaveBeenCalledWith( - expect.objectContaining({ - query: { - bool: { - filter: [ - { - term: { - 'resource.id': resourceId, - }, - }, - ], - must: [], - must_not: [], - should: [], - }, - }, - }) - ); - }); - - it('renders the empty state component', () => { - (useLatestVulnerabilities as jest.Mock).mockReturnValue({ - data: { total: 0, total_vulnerabilities: 0, page: [] }, - isLoading: false, - isFetching: false, - }); - - renderVulnerabilityByResource(); - expect(screen.getByText(/no results/i)).toBeInTheDocument(); - }); - - it('renders the Table', () => { - (useLatestVulnerabilities as jest.Mock).mockReturnValue({ - data: getResourceVulnerabilitiesMockData(), - isLoading: false, - isFetching: false, - }); - - renderVulnerabilityByResource(); - - // Header - expect(screen.getByText(/0d103e99f17f355ba/i)).toBeInTheDocument(); - expect(screen.getByText(/us-east-1/i)).toBeInTheDocument(); - expect( - screen.getByText(/elastic-agent-instance-a6c683d0-0977-11ee-bb0b-0af2059ffbbf/i) - ).toBeInTheDocument(); - - // Table - expect(screen.getByText(/CVE-2022-28948/i)).toBeInTheDocument(); - expect(screen.getByTestId(VULNERABILITIES_CVSS_SCORE_BADGE_SUBJ)).toHaveTextContent(/7.5/i); - expect(screen.getByTestId(VULNERABILITIES_CVSS_SCORE_BADGE_SUBJ)).toHaveTextContent(/v3/i); - expect(screen.getByText(/high/i)).toBeInTheDocument(); - expect(screen.getByText(/gopkg.in\/yaml.v3/i)).toBeInTheDocument(); - expect(screen.getByText(/v3.0.0-20210107192922-496545a6307b/i)).toBeInTheDocument(); - expect(screen.getByText(/3.0.0-20220521103104-8f96da9f5d5e/i)).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/resource_vulnerabilities.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/resource_vulnerabilities.tsx deleted file mode 100644 index 673dd2e8130e40..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/resource_vulnerabilities.tsx +++ /dev/null @@ -1,497 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { - EuiButtonEmpty, - EuiButtonIcon, - EuiDataGrid, - EuiDataGridCellValueElementProps, - EuiProgress, - EuiSpacer, - useEuiTheme, -} from '@elastic/eui'; -import { cx } from '@emotion/css'; -import { DataView } from '@kbn/data-views-plugin/common'; -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 { - CloudPostureTableResult, - useCloudPostureTable, -} from '../../../../common/hooks/use_cloud_posture_table'; -import { useLatestVulnerabilities } from '../../hooks/use_latest_vulnerabilities'; -import type { 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'; -import { EmptyState } from '../../../../components/empty_state'; -import { VulnerabilityFindingFlyout } from '../../vulnerabilities_finding_flyout/vulnerability_finding_flyout'; -import { useLimitProperties } from '../../../../common/utils/get_limit_properties'; -import { - LimitedResultsBar, - PageTitle, - PageTitleText, -} from '../../../configurations/layout/findings_layout'; -import { - getVulnerabilitiesColumnsGrid, - vulnerabilitiesColumns, -} from '../../vulnerabilities_table_columns'; -import { - defaultLoadingRenderer, - defaultNoDataRenderer, -} from '../../../../components/cloud_posture_page'; -import { SEARCH_BAR_PLACEHOLDER, VULNERABILITIES } from '../../translations'; -import { - severitySchemaConfig, - severitySortScript, - getCaseInsensitiveSortScript, -} from '../../utils/custom_sort_script'; -import { useStyles } from '../../hooks/use_styles'; -import { findingsNavigation } from '../../../../common/navigation/constants'; -import { CspInlineDescriptionList } from '../../../../components/csp_inline_description_list'; -import { getVulnerabilitiesGridCellActions } from '../../utils/get_vulnerabilities_grid_cell_actions'; - -const getDefaultQuery = ({ query, filters }: any) => ({ - query, - filters, - sort: [ - { id: vulnerabilitiesColumns.severity, direction: 'desc' }, - { id: vulnerabilitiesColumns.cvss, direction: 'desc' }, - ], - pageIndex: 0, -}); - -const ResourceVulnerabilitiesDataGrid = ({ - dataView, - data, - isFetching, - pageIndex, - sort, - pageSize, - onChangeItemsPerPage, - onChangePage, - onSort, - urlQuery, - setUrlQuery, - onResetFilters, -}: { - dataView: DataView; - data: VulnerabilitiesQueryData; - isFetching: boolean; -} & Pick< - CloudPostureTableResult, - | 'pageIndex' - | 'sort' - | 'pageSize' - | 'onChangeItemsPerPage' - | 'onChangePage' - | 'onSort' - | 'urlQuery' - | 'setUrlQuery' - | 'onResetFilters' ->) => { - const { euiTheme } = useEuiTheme(); - const styles = useStyles(); - - const [showHighlight, setHighlight] = useState(false); - - const onSortHandler = useCallback( - (newSort: any) => { - onSort(newSort); - if (newSort.length !== sort.length) { - setHighlight(true); - setTimeout(() => { - setHighlight(false); - }, 2000); - } - }, - [onSort, sort] - ); - - const invalidIndex = -1; - - const selectedVulnerability = useMemo(() => { - return data?.page[urlQuery.vulnerabilityIndex]; - }, [data?.page, urlQuery.vulnerabilityIndex]); - - const onCloseFlyout = () => { - setUrlQuery({ - vulnerabilityIndex: invalidIndex, - }); - }; - - const onOpenFlyout = useCallback( - (vulnerabilityRow: VulnerabilitiesQueryData['page'][number]) => { - const vulnerabilityIndex = data?.page.findIndex( - (vulnerabilityRecord: VulnerabilitiesQueryData['page'][number]) => - vulnerabilityRecord.vulnerability?.id === vulnerabilityRow.vulnerability?.id && - vulnerabilityRecord.resource?.id === vulnerabilityRow.resource?.id && - vulnerabilityRecord.package.name === vulnerabilityRow.package.name && - vulnerabilityRecord.package.version === vulnerabilityRow.package.version - ); - setUrlQuery({ - vulnerabilityIndex, - }); - }, - [setUrlQuery, data?.page] - ); - - const { isLastLimitedPage, limitedTotalItemCount } = useLimitProperties({ - total: data?.total, - pageIndex, - pageSize, - }); - - const columns = useMemo(() => { - if (!data?.page) { - return []; - } - - return getVulnerabilitiesGridCellActions({ - columnGridFn: getVulnerabilitiesColumnsGrid, - columns: vulnerabilitiesColumns, - dataView, - pageSize, - data: data.page, - setUrlQuery, - filters: urlQuery.filters, - }).filter( - (column) => - column.id !== vulnerabilitiesColumns.resourceName && - column.id !== vulnerabilitiesColumns.resourceId - ); - }, [data?.page, dataView, pageSize, setUrlQuery, urlQuery.filters]); - - const flyoutVulnerabilityIndex = urlQuery?.vulnerabilityIndex; - - const selectedVulnerabilityIndex = flyoutVulnerabilityIndex + pageIndex * pageSize; - - const renderCellValue = useMemo(() => { - const Cell: React.FC = ({ - columnId, - rowIndex, - setCellProps, - }): React.ReactElement | null => { - const rowIndexFromPage = rowIndex > pageSize - 1 ? rowIndex % pageSize : rowIndex; - - const vulnerabilityRow = data?.page[rowIndexFromPage]; - - useEffect(() => { - if (selectedVulnerabilityIndex === rowIndex) { - setCellProps({ - style: { - backgroundColor: euiTheme.colors.highlight, - }, - }); - } else { - setCellProps({ - style: { - backgroundColor: 'inherit', - }, - }); - } - }, [rowIndex, setCellProps]); - - if (isFetching) return null; - if (!vulnerabilityRow) return null; - if (!vulnerabilityRow.vulnerability?.id) return null; - - if (columnId === vulnerabilitiesColumns.actions) { - return ( - { - onOpenFlyout(vulnerabilityRow); - }} - /> - ); - } - if (columnId === vulnerabilitiesColumns.vulnerability) { - return <>{vulnerabilityRow.vulnerability?.id}; - } - if (columnId === vulnerabilitiesColumns.cvss) { - if ( - !vulnerabilityRow.vulnerability.score?.base || - !vulnerabilityRow.vulnerability.score?.version - ) { - return null; - } - return ( - - ); - } - if (columnId === vulnerabilitiesColumns.severity) { - if (!vulnerabilityRow.vulnerability.severity) { - return null; - } - return ; - } - - if (columnId === vulnerabilitiesColumns.package) { - return <>{vulnerabilityRow?.package?.name}; - } - if (columnId === vulnerabilitiesColumns.version) { - return <>{vulnerabilityRow?.package?.version}; - } - if (columnId === vulnerabilitiesColumns.fixedVersion) { - return <>{vulnerabilityRow?.package?.fixed_version}; - } - - return null; - }; - - return Cell; - }, [ - data?.page, - euiTheme.colors.highlight, - onOpenFlyout, - pageSize, - selectedVulnerabilityIndex, - isFetching, - ]); - - const onPaginateFlyout = useCallback( - (nextVulnerabilityIndex: number) => { - // the index of the vulnerability in the current page - const newVulnerabilityIndex = nextVulnerabilityIndex % pageSize; - - // if the vulnerability is not in the current page, we need to change the page - const flyoutPageIndex = Math.floor(nextVulnerabilityIndex / pageSize); - - setUrlQuery({ - pageIndex: flyoutPageIndex, - vulnerabilityIndex: newVulnerabilityIndex, - }); - }, - [pageSize, setUrlQuery] - ); - - const showVulnerabilityFlyout = flyoutVulnerabilityIndex > invalidIndex; - - if (data.page.length === 0) { - return ; - } - - return ( - <> - - 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: ( - <> - - {i18n.translate('xpack.csp.vulnerabilities.totalVulnerabilities', { - defaultMessage: - '{total, plural, one {# Vulnerability} other {# Vulnerabilities}}', - values: { total: data?.total }, - })} - - - ), - }, - }, - }} - 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 && } - {showVulnerabilityFlyout && selectedVulnerability && ( - - )} - - ); -}; -export const ResourceVulnerabilities = ({ dataView }: { dataView: DataView }) => { - const params = useParams<{ resourceId: string }>(); - const resourceId = decodeURIComponent(params.resourceId); - - const { - pageIndex, - pageSize, - onChangeItemsPerPage, - onChangePage, - query, - sort, - onSort, - queryError, - urlQuery, - setUrlQuery, - onResetFilters, - } = 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 (isLoading) { - return defaultLoadingRenderer(); - } - - if (!data?.page) { - return defaultNoDataRenderer(); - } - - return ( - <> - { - setUrlQuery({ ...newQuery, pageIndex: 0 }); - }} - loading={isFetching} - placeholder={SEARCH_BAR_PLACEHOLDER} - /> - - - - - - - - - - - - - - {error && } - {!error && ( - - )} - - ); -}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/test_subjects.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/test_subjects.ts deleted file mode 100644 index 027b0b1cdb2edb..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/test_subjects.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const VULNERABILITY_RESOURCE_COUNT = 'vulnerability_resource_count'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource.test.tsx deleted file mode 100644 index bd3fb0913bc3e4..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource.test.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { VulnerabilitiesByResource } from './vulnerabilities_by_resource'; -import { TestProvider } from '../../../test/test_provider'; -import { useLatestVulnerabilitiesByResource } from '../hooks/use_latest_vulnerabilities_by_resource'; -import { VULNERABILITY_RESOURCE_COUNT } from './test_subjects'; -import { getVulnerabilitiesByResourceData } from './__mocks__/vulnerabilities_by_resource.mock'; - -jest.mock('../hooks/use_latest_vulnerabilities_by_resource', () => ({ - useLatestVulnerabilitiesByResource: jest.fn(), -})); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe('VulnerabilitiesByResource', () => { - const dataView: any = {}; - - const renderVulnerabilityByResource = () => { - return render( - - - - ); - }; - - it('renders the loading state', () => { - (useLatestVulnerabilitiesByResource as jest.Mock).mockReturnValue({ - data: undefined, - isLoading: true, - isFetching: true, - }); - renderVulnerabilityByResource(); - expect(screen.getByText(/loading/i)).toBeInTheDocument(); - }); - it('renders the no data state', () => { - (useLatestVulnerabilitiesByResource as jest.Mock).mockReturnValue({ - data: undefined, - isLoading: false, - isFetching: false, - }); - - renderVulnerabilityByResource(); - expect(screen.getByText(/no data/i)).toBeInTheDocument(); - }); - - it('renders the empty state component', () => { - (useLatestVulnerabilitiesByResource as jest.Mock).mockReturnValue({ - data: { total: 0, total_vulnerabilities: 0, page: [] }, - isLoading: false, - isFetching: false, - }); - - renderVulnerabilityByResource(); - expect(screen.getByText(/no results/i)).toBeInTheDocument(); - }); - - it('renders the Table', () => { - (useLatestVulnerabilitiesByResource as jest.Mock).mockReturnValue({ - data: getVulnerabilitiesByResourceData(), - isLoading: false, - isFetching: false, - }); - - renderVulnerabilityByResource(); - expect(screen.getByText(/2 resources/i)).toBeInTheDocument(); - expect(screen.getByText(/8 vulnerabilities/i)).toBeInTheDocument(); - expect(screen.getByText(/resource-id-1/i)).toBeInTheDocument(); - expect(screen.getByText(/resource-id-2/i)).toBeInTheDocument(); - expect(screen.getByText(/resource-test-1/i)).toBeInTheDocument(); - expect(screen.getAllByText(/us-test-1/i)).toHaveLength(2); - expect(screen.getAllByTestId(VULNERABILITY_RESOURCE_COUNT)).toHaveLength(2); - expect(screen.getAllByTestId(VULNERABILITY_RESOURCE_COUNT)[0]).toHaveTextContent('4'); - expect(screen.getAllByTestId(VULNERABILITY_RESOURCE_COUNT)[1]).toHaveTextContent('4'); - }); -}); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource.tsx deleted file mode 100644 index 89488bf52046b8..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource.tsx +++ /dev/null @@ -1,309 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { - EuiBadge, - EuiButtonEmpty, - EuiDataGrid, - EuiDataGridCellValueElementProps, - EuiFlexItem, - EuiProgress, - EuiSpacer, -} from '@elastic/eui'; -import { DataView } from '@kbn/data-views-plugin/common'; -import React, { useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { Link, generatePath } from 'react-router-dom'; -import { LOCAL_STORAGE_PAGE_SIZE_FINDINGS_KEY } from '../../../common/constants'; -import { findingsNavigation } from '../../../common/navigation/constants'; -import { - CloudPostureTableResult, - useCloudPostureTable, -} from '../../../common/hooks/use_cloud_posture_table'; -import { ErrorCallout } from '../../configurations/layout/error_callout'; -import { FindingsSearchBar } from '../../configurations/layout/findings_search_bar'; -import { useLimitProperties } from '../../../common/utils/get_limit_properties'; -import { LimitedResultsBar } from '../../configurations/layout/findings_layout'; -import { - getVulnerabilitiesByResourceColumnsGrid, - vulnerabilitiesByResourceColumns, -} from './vulnerabilities_by_resource_table_columns'; -import { - defaultLoadingRenderer, - defaultNoDataRenderer, -} from '../../../components/cloud_posture_page'; -import { SEARCH_BAR_PLACEHOLDER, VULNERABILITIES } from '../translations'; -import { useStyles } from '../hooks/use_styles'; -import { FindingsGroupBySelector } from '../../configurations/layout/findings_group_by_selector'; -import { vulnerabilitiesPathnameHandler } from '../utils/vulnerabilities_pathname_handler'; -import { useLatestVulnerabilitiesByResource } from '../hooks/use_latest_vulnerabilities_by_resource'; -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, - filters, - sort: [{ id: vulnerabilitiesByResourceColumns.vulnerabilities_count, direction: 'desc' }], - pageIndex: 0, -}); - -const VulnerabilitiesByResourceDataGrid = ({ - dataView, - data, - isFetching, - pageIndex, - sort, - pageSize, - onChangeItemsPerPage, - onChangePage, - onSort, - urlQuery, - setUrlQuery, - onResetFilters, -}: { - dataView: DataView; - data: VulnerabilitiesByResourceQueryData | undefined; - isFetching: boolean; -} & Pick< - CloudPostureTableResult, - | 'pageIndex' - | 'sort' - | 'pageSize' - | 'onChangeItemsPerPage' - | 'onChangePage' - | 'onSort' - | 'urlQuery' - | 'setUrlQuery' - | 'onResetFilters' ->) => { - const styles = useStyles(); - - const { isLastLimitedPage, limitedTotalItemCount } = useLimitProperties({ - total: data?.total, - pageIndex, - pageSize, - }); - - const columns = useMemo(() => { - if (!data?.page) { - return []; - } - return getVulnerabilitiesGridCellActions({ - columnGridFn: getVulnerabilitiesByResourceColumnsGrid, - columns: vulnerabilitiesByResourceColumns, - dataView, - pageSize, - data: data.page, - setUrlQuery, - filters: urlQuery.filters, - }); - }, [data, dataView, pageSize, setUrlQuery, urlQuery.filters]); - - const renderCellValue = useMemo(() => { - const Cell: React.FC = ({ - columnId, - rowIndex, - }): React.ReactElement | null => { - const rowIndexFromPage = rowIndex > pageSize - 1 ? rowIndex % pageSize : rowIndex; - - const resourceVulnerabilityRow = data?.page[rowIndexFromPage]; - - if (isFetching) return null; - if (!resourceVulnerabilityRow?.resource?.id) return null; - - if (columnId === vulnerabilitiesByResourceColumns.resourceId) { - return ( - - {resourceVulnerabilityRow?.resource?.id} - - ); - } - if (columnId === vulnerabilitiesByResourceColumns.resourceName) { - return <>{resourceVulnerabilityRow?.resource?.name}; - } - if (columnId === vulnerabilitiesByResourceColumns.region) { - return <>{resourceVulnerabilityRow?.cloud?.region}; - } - if (columnId === vulnerabilitiesByResourceColumns.vulnerabilities_count) { - return ( - - {resourceVulnerabilityRow.vulnerabilities_count} - - ); - } - - if (columnId === vulnerabilitiesByResourceColumns.severity_map) { - return ( - - ); - } - return null; - }; - - return Cell; - }, [data?.page, pageSize, isFetching]); - - if (data?.page.length === 0) { - return ; - } - - return ( - <> - - id), - setVisibleColumns: () => {}, - }} - rowCount={limitedTotalItemCount} - toolbarVisibility={{ - showColumnSelector: false, - showDisplaySelector: false, - showKeyboardShortcuts: false, - showSortSelector: false, - showFullScreenSelector: false, - additionalControls: { - left: { - prepend: ( - <> - - {i18n.translate('xpack.csp.vulnerabilitiesByResource.totalResources', { - defaultMessage: '{total, plural, one {# Resource} other {# Resources}}', - values: { total: data?.total }, - })} - - - {i18n.translate('xpack.csp.vulnerabilitiesByResource.totalVulnerabilities', { - defaultMessage: - '{total, plural, one {# Vulnerability} other {# Vulnerabilities}}', - values: { total: data?.total_vulnerabilities }, - })} - - - ), - }, - right: ( - - - - ), - }, - }} - 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 && } - - ); -}; - -export const VulnerabilitiesByResource = ({ dataView }: { dataView: DataView }) => { - const { - pageIndex, - onChangeItemsPerPage, - onChangePage, - pageSize, - query, - sort, - onSort, - queryError, - urlQuery, - setUrlQuery, - onResetFilters, - } = 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 (isLoading && !error) { - return defaultLoadingRenderer(); - } - - if (!data?.page && !error) { - return defaultNoDataRenderer(); - } - - return ( - <> - { - setUrlQuery({ ...newQuery, pageIndex: 0 }); - }} - loading={isFetching} - placeholder={SEARCH_BAR_PLACEHOLDER} - /> - - {error && } - {!error && ( - - )} - - ); -}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource_table_columns.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource_table_columns.ts deleted file mode 100644 index 42196f151bd074..00000000000000 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource_table_columns.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiDataGridColumn, EuiDataGridColumnCellAction } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -export const vulnerabilitiesByResourceColumns = { - resourceId: 'resource.id', - resourceName: 'resource.name', - region: 'cloud.region', - vulnerabilities_count: 'vulnerabilities_count', - severity_map: 'severity_map', -}; - -const defaultColumnProps = (): Partial => ({ - isExpandable: false, - actions: { - showHide: false, - showMoveLeft: false, - showMoveRight: false, - showSortAsc: false, - showSortDesc: false, - }, - isSortable: false, -}); - -export const getVulnerabilitiesByResourceColumnsGrid = ( - cellActions: EuiDataGridColumnCellAction[] -): EuiDataGridColumn[] => [ - { - ...defaultColumnProps(), - id: vulnerabilitiesByResourceColumns.resourceId, - displayAsText: i18n.translate('xpack.csp.vulnerabilityByResourceTable.column.resourceId', { - defaultMessage: 'Resource ID', - }), - cellActions, - }, - { - ...defaultColumnProps(), - id: vulnerabilitiesByResourceColumns.resourceName, - displayAsText: i18n.translate('xpack.csp.vulnerabilityByResourceTable.column.resourceName', { - defaultMessage: 'Resource Name', - }), - cellActions, - }, - { - ...defaultColumnProps(), - id: vulnerabilitiesByResourceColumns.region, - displayAsText: i18n.translate('xpack.csp.vulnerabilityByResourceTable.column.region', { - defaultMessage: 'Region', - }), - cellActions, - initialWidth: 150, - }, - { - ...defaultColumnProps(), - actions: { - showHide: false, - showMoveLeft: false, - showMoveRight: false, - showSortAsc: true, - showSortDesc: true, - }, - id: vulnerabilitiesByResourceColumns.vulnerabilities_count, - displayAsText: i18n.translate('xpack.csp.vulnerabilityByResourceTable.column.vulnerabilities', { - defaultMessage: 'Vulnerabilities', - }), - initialWidth: 140, - isResizable: false, - isSortable: true, - }, - { - ...defaultColumnProps(), - id: vulnerabilitiesByResourceColumns.severity_map, - displayAsText: i18n.translate('xpack.csp.vulnerabilityByResourceTable.column.severityMap', { - defaultMessage: 'Severity Map', - }), - cellActions, - initialWidth: 110, - isResizable: false, - }, -]; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.test.tsx index c1ffdfeb41914a..da7587cfc8ad03 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.test.tsx @@ -42,7 +42,7 @@ describe('', () => { expect(descriptionList.textContent).toEqual( `Resource ID:${mockVulnerabilityHit.resource?.id}Resource Name:${mockVulnerabilityHit.resource?.name}Package:${mockVulnerabilityHit.package.name}Version:${mockVulnerabilityHit.package.version}` ); - getByText(mockVulnerabilityHit.vulnerability.severity); + getByText(mockVulnerabilityHit.vulnerability.severity!); }); }); @@ -93,19 +93,24 @@ describe('', () => { }); }); - it('should allow pagination with next', async () => { - const { getByTestId } = render(); + /** + * TODO: Enable this test once https://github.com/elastic/kibana/issues/168619 is resolved + */ + describe.skip('Flyout Pagination', () => { + it('should allow pagination with next', async () => { + const { getByTestId } = render(); - userEvent.click(getByTestId('pagination-button-next')); + userEvent.click(getByTestId('pagination-button-next')); - expect(onPaginate).toHaveBeenCalledWith(1); - }); + expect(onPaginate).toHaveBeenCalledWith(1); + }); - it('should allow pagination with previous', async () => { - const { getByTestId } = render(); + it('should allow pagination with previous', async () => { + const { getByTestId } = render(); - userEvent.click(getByTestId('pagination-button-previous')); + userEvent.click(getByTestId('pagination-button-previous')); - expect(onPaginate).toHaveBeenCalledWith(0); + expect(onPaginate).toHaveBeenCalledWith(0); + }); }); }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx index ac8c98e87f4118..06b8fdc2ad941c 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx @@ -81,18 +81,18 @@ const getFlyoutDescriptionList = ( export const VulnerabilityFindingFlyout = ({ closeFlyout, + vulnerabilityRecord, onPaginate, totalVulnerabilitiesCount, flyoutIndex, - vulnerabilityRecord, - isLoading, + isLoading = false, }: { closeFlyout: () => void; + vulnerabilityRecord: CspVulnerabilityFinding; onPaginate?: (pageIndex: number) => void; - totalVulnerabilitiesCount: number; + totalVulnerabilitiesCount?: number; flyoutIndex?: number; - vulnerabilityRecord: CspVulnerabilityFinding; - isLoading: boolean; + isLoading?: boolean; }) => { const [selectedTabId, setSelectedTabId] = useState(overviewTabId); const vulnerability = vulnerabilityRecord?.vulnerability; @@ -241,7 +241,7 @@ export const VulnerabilityFindingFlyout = ({ alignItems="center" justifyContent={onPaginate ? 'spaceBetween' : 'flexEnd'} > - {onPaginate && ( + {onPaginate && totalVulnerabilitiesCount && flyoutIndex && ( ({ - isExpandable: false, - actions: { - showHide: false, - showMoveLeft: false, - showMoveRight: false, - }, -}); - -export const getVulnerabilitiesColumnsGrid = ( - cellActions: EuiDataGridColumnCellAction[] -): EuiDataGridColumn[] => [ - { - ...defaultColumnProps(), - id: vulnerabilitiesColumns.actions, - initialWidth: 40, - display: [], - actions: false, - isSortable: false, - isResizable: false, - cellActions: [], - }, - { - ...defaultColumnProps(), - id: vulnerabilitiesColumns.vulnerability, - displayAsText: i18n.translate('xpack.csp.vulnerabilityTable.column.vulnerability', { - defaultMessage: 'Vulnerability', - }), - initialWidth: 130, - cellActions, - }, - { - ...defaultColumnProps(), - id: vulnerabilitiesColumns.cvss, - displayAsText: 'CVSS', - initialWidth: 80, - isResizable: false, - cellActions, - }, - { - ...defaultColumnProps(), - id: vulnerabilitiesColumns.resourceId, - displayAsText: i18n.translate('xpack.csp.vulnerabilityTable.column.resourceId', { - defaultMessage: 'Resource ID', - }), - cellActions, - }, - { - ...defaultColumnProps(), - id: vulnerabilitiesColumns.resourceName, - displayAsText: i18n.translate('xpack.csp.vulnerabilityTable.column.resourceName', { - defaultMessage: 'Resource Name', - }), - cellActions, - }, - { - ...defaultColumnProps(), - id: vulnerabilitiesColumns.severity, - displayAsText: i18n.translate('xpack.csp.vulnerabilityTable.column.severity', { - defaultMessage: 'Severity', - }), - initialWidth: 100, - cellActions, - schema: severitySchemaConfig.type, - }, - { - ...defaultColumnProps(), - id: vulnerabilitiesColumns.package, - displayAsText: i18n.translate('xpack.csp.vulnerabilityTable.column.package', { - defaultMessage: 'Package', - }), - cellActions, - }, - { - ...defaultColumnProps(), - id: vulnerabilitiesColumns.version, - displayAsText: i18n.translate('xpack.csp.vulnerabilityTable.column.version', { - defaultMessage: 'Version', - }), - cellActions, - }, - { - ...defaultColumnProps(), - id: vulnerabilitiesColumns.fixedVersion, - displayAsText: i18n.translate('xpack.csp.vulnerabilityTable.column.fixVersion', { - defaultMessage: 'Fix Version', - }), - cellActions, - }, -]; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 0d684afb7809c2..dcf92f6e1670ae 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -11903,13 +11903,7 @@ "xpack.csp.cloudPosturePage.kspmIntegration.packageNotInstalled.description": "Utilisez notre intégration {integrationFullName} (KSPM) pour détecter les erreurs de configuration de sécurité dans vos clusters Kubernetes.", "xpack.csp.complianceScoreBar.tooltipTitle": "{failed} résultats en échec et {passed} ayant réussi", "xpack.csp.eksIntegration.docsLink": "Lisez {docs} pour en savoir plus", - "xpack.csp.findings..bottomBarLabel": "Voici les {maxItems} premiers résultats correspondant à votre recherche. Veuillez l'affiner pour en voir davantage.", "xpack.csp.findings.distributionBar.showingPageOfTotalLabel": "Affichage de {pageStart}-{pageEnd} sur {total} {type}", - "xpack.csp.findings.findingsTableCell.addFilterButton": "Ajouter un filtre {field}", - "xpack.csp.findings.findingsTableCell.addFilterButtonTooltip": "Ajouter un filtre {field}", - "xpack.csp.findings.findingsTableCell.addNegatedFilterButtonTooltip": "Ajouter un filtre {field} négatif", - "xpack.csp.findings.findingsTableCell.addNegateFilterButton": "Ajouter un filtre {field} négatif", - "xpack.csp.findings.resourceFindings.resourceFindingsPageTitle": "{resourceName} {hyphen} Résultats", "xpack.csp.findingsFlyout.alerts.alertCount": "{alertCount, plural, one {# alerte} many {# alertes} other {Alertes #}}", "xpack.csp.findingsFlyout.alerts.detectionRuleCount": "{ruleCount, plural, one {# règle de détection} many {# règles de détection} other {# règles de détection}}", "xpack.csp.noFindingsStates.indexTimeout.indexTimeoutDescription": "La collecte des résultats prend plus de temps que prévu. {docs}.", @@ -11917,15 +11911,7 @@ "xpack.csp.rules.rulesTable.showingPageOfTotalLabel": "Affichage de {pageSize} sur {total, plural, one {# règle} many {# règles bien mises} other {# règles}}", "xpack.csp.subscriptionNotAllowed.promptDescription": "Pour utiliser ces fonctionnalités de sécurité du cloud, vous devez {link}.", "xpack.csp.vulnerabilities.detectionRuleNamePrefix": "Vulnérabilité : {vulnerabilityId}", - "xpack.csp.vulnerabilities.resourceVulnerabilities.vulnerabilitiesPageTitle": "{resourceName} {hyphen} vulnérabilités", - "xpack.csp.vulnerabilities.totalVulnerabilities": "{total, plural, one {# vulnérabilité} many {# vulnérabilités} other {# vulnérabilités}}", - "xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addFilterButton": "Ajouter un filtre {columnId}", - "xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addFilterButtonTooltip": "Ajouter un filtre {columnId}", - "xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addNegatedFilterButtonTooltip": "Ajouter un filtre {columnId} négatif", - "xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addNegateFilterButton": "Ajouter un filtre {columnId} négatif", "xpack.csp.vulnerabilities.vulnerabilityOverviewTile.publishedDateText": "{date}", - "xpack.csp.vulnerabilitiesByResource.totalResources": "{total, plural, one {# ressource} many {# ressources} other {# ressources}}", - "xpack.csp.vulnerabilitiesByResource.totalVulnerabilities": "{total, plural, one {# vulnérabilité} many {# vulnérabilités} other {# vulnérabilités}}", "xpack.csp.awsIntegration.accessKeyIdLabel": "ID de clé d'accès", "xpack.csp.awsIntegration.assumeRoleDescription": "Un nom ARN (Amazon Resource Name) de rôle IAM est une identité IAM que vous pouvez créer dans votre compte AWS. Lors de la création d'un rôle IAM, les utilisateurs peuvent définir les autorisations accordées au rôle. Les rôles n'ont pas d'informations d'identification à long terme standard telles que des mots de passe ou des clés d'accès.", "xpack.csp.awsIntegration.assumeRoleLabel": "Assumer un rôle", @@ -12060,15 +12046,10 @@ "xpack.csp.emptyState.readDocsLink": "Lisez les documents", "xpack.csp.emptyState.resetFiltersButton": "Réinitialiser les filtres", "xpack.csp.emptyState.title": "Aucun résultat ne correspond à vos critères de recherche.", - "xpack.csp.expandColumnDescriptionLabel": "Développer", - "xpack.csp.expandColumnNameLabel": "Développer", "xpack.csp.findings.distributionBar.totalFailedLabel": "Échec des résultats", "xpack.csp.findings.distributionBar.totalPassedLabel": "Réussite des résultats", "xpack.csp.findings.errorCallout.pageSearchErrorTitle": "Une erreur s’est produite lors de la récupération des résultats de recherche.", "xpack.csp.findings.errorCallout.showErrorButtonLabel": "Afficher le message d'erreur", - "xpack.csp.findings.findingsByResource.tableRowTypeLabel": "Ressources", - "xpack.csp.findings.findingsByResourceTable.cisSectionsColumnLabel": "Sections CIS", - "xpack.csp.findings.findingsByResourceTable.postureScoreColumnLabel": "Score du niveau", "xpack.csp.findings.findingsErrorToast.searchFailedTitle": "Échec de la recherche", "xpack.csp.findings.findingsFlyout.jsonTabTitle": "JSON", "xpack.csp.findings.findingsFlyout.overviewTab.alertsTitle": "Alertes", @@ -12100,16 +12081,11 @@ "xpack.csp.findings.findingsFlyout.ruleTab.tagsTitle": "Balises", "xpack.csp.findings.findingsFlyout.ruleTabTitle": "Règle", "xpack.csp.findings.findingsFlyout.tableTabTitle": "Tableau", - "xpack.csp.findings.findingsTable.findingsTableColumn.clusterIdColumnLabel": "Appartient à", - "xpack.csp.findings.findingsTable.findingsTableColumn.clusterIdColumnTooltipLabel": "ID de cluster Kubernetes ou nom de compte cloud", "xpack.csp.findings.findingsTable.findingsTableColumn.lastCheckedColumnLabel": "Dernière vérification", "xpack.csp.findings.findingsTable.findingsTableColumn.resourceIdColumnLabel": "ID ressource", - "xpack.csp.findings.findingsTable.findingsTableColumn.resourceIdColumnTooltipLabel": "ID ressource Elastic personnalisée", "xpack.csp.findings.findingsTable.findingsTableColumn.resourceNameColumnLabel": "Nom de ressource", "xpack.csp.findings.findingsTable.findingsTableColumn.resourceTypeColumnLabel": "Type de ressource", "xpack.csp.findings.findingsTable.findingsTableColumn.resultColumnLabel": "Résultat", - "xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnLabel": "Benchmark applicable", - "xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnTooltipLabel": "Le benchmark utilisé pour évaluer cette ressource", "xpack.csp.findings.findingsTable.findingsTableColumn.ruleNameColumnLabel": "Nom de règle", "xpack.csp.findings.findingsTable.findingsTableColumn.ruleNumberColumnLabel": "Numéro de règle", "xpack.csp.findings.findingsTable.findingsTableColumn.ruleSectionColumnLabel": "Section CIS", @@ -12120,12 +12096,6 @@ "xpack.csp.findings.groupBySelector.groupByNoneLabel": "Aucun", "xpack.csp.findings.groupBySelector.groupByResourceIdLabel": "Ressource", "xpack.csp.findings.latestFindings.tableRowTypeLabel": "Résultats", - "xpack.csp.findings.resourceFindings.backToResourcesPageButtonLabel": "Retour aux ressources", - "xpack.csp.findings.resourceFindings.tableRowTypeLabel": "Résultats", - "xpack.csp.findings.resourceFindingsSharedValues.cloudAccountName": "Nom du compte cloud", - "xpack.csp.findings.resourceFindingsSharedValues.clusterIdTitle": "ID cluster", - "xpack.csp.findings.resourceFindingsSharedValues.resourceIdTitle": "ID ressource", - "xpack.csp.findings.resourceFindingsSharedValues.resourceTypeTitle": "Type de ressource", "xpack.csp.findings.search.queryErrorToastMessage": "Erreur de requête", "xpack.csp.findings.searchBar.searchPlaceholder": "Rechercher dans les résultats (par ex. rule.section : \"serveur d'API\")", "xpack.csp.findings.tabs.misconfigurations": "Configurations incorrectes", @@ -12240,9 +12210,6 @@ "xpack.csp.vulnerabilities": "Vulnérabilités", "xpack.csp.vulnerabilities.flyoutTabs.fieldLabel": "Champ", "xpack.csp.vulnerabilities.flyoutTabs.fieldValueLabel": "Valeur", - "xpack.csp.vulnerabilities.resourceVulnerabilities.backToResourcesPageButtonLabel": "Retour aux ressources", - "xpack.csp.vulnerabilities.resourceVulnerabilities.regionTitle": "Région", - "xpack.csp.vulnerabilities.resourceVulnerabilities.resourceIdTitle": "ID ressource", "xpack.csp.vulnerabilities.searchBar.placeholder": "Rechercher des vulnérabilités (par exemple vulnerability.severity : \"CRITICAL\" )", "xpack.csp.vulnerabilities.table.filterIn": "Inclure", "xpack.csp.vulnerabilities.table.filterOut": "Exclure", @@ -12265,24 +12232,12 @@ "xpack.csp.vulnerabilities.vulnerabilityOverviewTile.publishedDate": "Date de publication", "xpack.csp.vulnerabilitiesByResource.severityMap.tooltipTitle": "Carte des degrés de gravité", "xpack.csp.vulnerability_dashboard.cspPageTemplate.pageTitle": "Gestion des vulnérabilités natives du cloud", - "xpack.csp.vulnerabilityByResourceTable.column.region": "Région", - "xpack.csp.vulnerabilityByResourceTable.column.resourceId": "ID ressource", - "xpack.csp.vulnerabilityByResourceTable.column.resourceName": "Nom de ressource", - "xpack.csp.vulnerabilityByResourceTable.column.severityMap": "Carte des degrés de gravité", - "xpack.csp.vulnerabilityByResourceTable.column.vulnerabilities": "Vulnérabilités", "xpack.csp.vulnerabilityDashboard.trendGraphChart.accountsDropDown.option.allTitle": "Tous", "xpack.csp.vulnerabilityDashboard.trendGraphChart.accountsDropDown.prepend.accountsTitle": "Comptes", "xpack.csp.vulnerabilityDashboard.trendGraphChart.trendBySeverityTitle": "Tendance par degré de gravité", "xpack.csp.vulnerabilityDashboard.viewAllButton.buttonTitle": "Tout afficher", - "xpack.csp.vulnerabilityTable.column.fixVersion": "Version du correctif", - "xpack.csp.vulnerabilityTable.column.package": "Pack", - "xpack.csp.vulnerabilityTable.column.resourceId": "ID ressource", - "xpack.csp.vulnerabilityTable.column.resourceName": "Nom de ressource", - "xpack.csp.vulnerabilityTable.column.severity": "Sévérité", "xpack.csp.vulnerabilityTable.column.sortAscending": "Basse -> Critique", "xpack.csp.vulnerabilityTable.column.sortDescending": "Critique -> Basse", - "xpack.csp.vulnerabilityTable.column.version": "Version", - "xpack.csp.vulnerabilityTable.column.vulnerability": "Vulnérabilité", "xpack.csp.vulnerabilityTable.panel.buttonText": "Afficher toutes les vulnérabilités", "xpack.csp.vulnMgmtIntegration.awsOption.nameTitle": "Amazon Web Services", "xpack.csp.vulnMgmtIntegration.azureOption.nameTitle": "Azure", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 90240386985cf5..5cf4410604fc46 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11917,13 +11917,7 @@ "xpack.csp.cloudPosturePage.kspmIntegration.packageNotInstalled.description": "{integrationFullName}(CSPM)統合を使用して、Kubernetesクラスターの構成エラーを検出します。", "xpack.csp.complianceScoreBar.tooltipTitle": "{failed}が失敗し、{passed}が調査結果に合格しました", "xpack.csp.eksIntegration.docsLink": "詳細は{docs}をご覧ください", - "xpack.csp.findings..bottomBarLabel": "これらは検索条件に一致した初めの{maxItems}件の調査結果です。他の結果を表示するには検索条件を絞ってください。", "xpack.csp.findings.distributionBar.showingPageOfTotalLabel": "{total} {type}ページ中{pageStart}-{pageEnd}ページを表示中", - "xpack.csp.findings.findingsTableCell.addFilterButton": "{field}フィルターを追加", - "xpack.csp.findings.findingsTableCell.addFilterButtonTooltip": "{field}フィルターを追加", - "xpack.csp.findings.findingsTableCell.addNegatedFilterButtonTooltip": "{field}否定フィルターを追加", - "xpack.csp.findings.findingsTableCell.addNegateFilterButton": "{field}否定フィルターを追加", - "xpack.csp.findings.resourceFindings.resourceFindingsPageTitle": "{resourceName} {hyphen}調査結果", "xpack.csp.findingsFlyout.alerts.alertCount": "{alertCount, plural, other {#件のアラート}}", "xpack.csp.findingsFlyout.alerts.detectionRuleCount": "{ruleCount, plural, other {#検出ルール}}", "xpack.csp.noFindingsStates.indexTimeout.indexTimeoutDescription": "調査結果の収集に想定よりも時間がかかっています。{docs}。", @@ -11931,15 +11925,7 @@ "xpack.csp.rules.rulesTable.showingPageOfTotalLabel": "{total, plural, other {#個のルール}} 件中{pageSize}を表示中", "xpack.csp.subscriptionNotAllowed.promptDescription": "これらのクラウドセキュリティ機能を使用するには、{link}する必要があります。", "xpack.csp.vulnerabilities.detectionRuleNamePrefix": "脆弱性:{vulnerabilityId}", - "xpack.csp.vulnerabilities.resourceVulnerabilities.vulnerabilitiesPageTitle": "{resourceName} {hyphen} 脆弱性", - "xpack.csp.vulnerabilities.totalVulnerabilities": "{total, plural, other {#件の脆弱性}}", - "xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addFilterButton": "{columnId}フィルターを追加", - "xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addFilterButtonTooltip": "{columnId}フィルターを追加", - "xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addNegatedFilterButtonTooltip": "{columnId}否定フィルターを追加", - "xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addNegateFilterButton": "{columnId}否定フィルターを追加", "xpack.csp.vulnerabilities.vulnerabilityOverviewTile.publishedDateText": "{date}", - "xpack.csp.vulnerabilitiesByResource.totalResources": "{total, plural, other {#個のリソース}}", - "xpack.csp.vulnerabilitiesByResource.totalVulnerabilities": "{total, plural, other {#件の脆弱性}}", "xpack.csp.awsIntegration.accessKeyIdLabel": "アクセスキーID", "xpack.csp.awsIntegration.assumeRoleDescription": "IAMロールAmazon Resource Name(ARN)は、AWSアカウントで作成できるIAM IDです。IAMロールを作成するときには、ユーザーはロールの権限を定義できます。ロールには、パスワードやアクセスキーなどの標準の長期的な資格情報がありません。", "xpack.csp.awsIntegration.assumeRoleLabel": "ロールを想定", @@ -12074,15 +12060,10 @@ "xpack.csp.emptyState.readDocsLink": "ドキュメントを読む", "xpack.csp.emptyState.resetFiltersButton": "フィルターをリセット", "xpack.csp.emptyState.title": "検索条件と一致する結果がありません。", - "xpack.csp.expandColumnDescriptionLabel": "拡張", - "xpack.csp.expandColumnNameLabel": "拡張", "xpack.csp.findings.distributionBar.totalFailedLabel": "失敗した調査結果", "xpack.csp.findings.distributionBar.totalPassedLabel": "合格した調査結果", "xpack.csp.findings.errorCallout.pageSearchErrorTitle": "検索結果の取得中にエラーが発生しました", "xpack.csp.findings.errorCallout.showErrorButtonLabel": "エラーメッセージを表示", - "xpack.csp.findings.findingsByResource.tableRowTypeLabel": "リソース", - "xpack.csp.findings.findingsByResourceTable.cisSectionsColumnLabel": "CISセクション", - "xpack.csp.findings.findingsByResourceTable.postureScoreColumnLabel": "態勢スコア", "xpack.csp.findings.findingsErrorToast.searchFailedTitle": "検索失敗", "xpack.csp.findings.findingsFlyout.jsonTabTitle": "JSON", "xpack.csp.findings.findingsFlyout.overviewTab.alertsTitle": "アラート", @@ -12114,16 +12095,11 @@ "xpack.csp.findings.findingsFlyout.ruleTab.tagsTitle": "タグ", "xpack.csp.findings.findingsFlyout.ruleTabTitle": "ルール", "xpack.csp.findings.findingsFlyout.tableTabTitle": "表", - "xpack.csp.findings.findingsTable.findingsTableColumn.clusterIdColumnLabel": "属します", - "xpack.csp.findings.findingsTable.findingsTableColumn.clusterIdColumnTooltipLabel": "KubernetesクラスターIDまたはクラウドアカウント名", "xpack.csp.findings.findingsTable.findingsTableColumn.lastCheckedColumnLabel": "最終確認", "xpack.csp.findings.findingsTable.findingsTableColumn.resourceIdColumnLabel": "リソースID", - "xpack.csp.findings.findingsTable.findingsTableColumn.resourceIdColumnTooltipLabel": "カスタムElasticリソースID", "xpack.csp.findings.findingsTable.findingsTableColumn.resourceNameColumnLabel": "リソース名", "xpack.csp.findings.findingsTable.findingsTableColumn.resourceTypeColumnLabel": "リソースタイプ", "xpack.csp.findings.findingsTable.findingsTableColumn.resultColumnLabel": "結果", - "xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnLabel": "適用されるベンチマーク", - "xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnTooltipLabel": "このリソースの評価に使用されるベンチマーク", "xpack.csp.findings.findingsTable.findingsTableColumn.ruleNameColumnLabel": "ルール名", "xpack.csp.findings.findingsTable.findingsTableColumn.ruleNumberColumnLabel": "ルール番号", "xpack.csp.findings.findingsTable.findingsTableColumn.ruleSectionColumnLabel": "CISセクション", @@ -12134,12 +12110,6 @@ "xpack.csp.findings.groupBySelector.groupByNoneLabel": "なし", "xpack.csp.findings.groupBySelector.groupByResourceIdLabel": "リソース", "xpack.csp.findings.latestFindings.tableRowTypeLabel": "調査結果", - "xpack.csp.findings.resourceFindings.backToResourcesPageButtonLabel": "リソースに戻る", - "xpack.csp.findings.resourceFindings.tableRowTypeLabel": "調査結果", - "xpack.csp.findings.resourceFindingsSharedValues.cloudAccountName": "クラウドアカウント名", - "xpack.csp.findings.resourceFindingsSharedValues.clusterIdTitle": "クラスターID", - "xpack.csp.findings.resourceFindingsSharedValues.resourceIdTitle": "リソースID", - "xpack.csp.findings.resourceFindingsSharedValues.resourceTypeTitle": "リソースタイプ", "xpack.csp.findings.search.queryErrorToastMessage": "クエリエラー", "xpack.csp.findings.searchBar.searchPlaceholder": "検索結果(例:rule.section:\"API Server\")", "xpack.csp.findings.tabs.misconfigurations": "構成エラー", @@ -12254,9 +12224,6 @@ "xpack.csp.vulnerabilities": "脆弱性", "xpack.csp.vulnerabilities.flyoutTabs.fieldLabel": "フィールド", "xpack.csp.vulnerabilities.flyoutTabs.fieldValueLabel": "値", - "xpack.csp.vulnerabilities.resourceVulnerabilities.backToResourcesPageButtonLabel": "リソースに戻る", - "xpack.csp.vulnerabilities.resourceVulnerabilities.regionTitle": "地域", - "xpack.csp.vulnerabilities.resourceVulnerabilities.resourceIdTitle": "リソースID", "xpack.csp.vulnerabilities.searchBar.placeholder": "脆弱性を検索(例:vulnerability.severity :\"CRITICAL\")", "xpack.csp.vulnerabilities.table.filterIn": "フィルタリング", "xpack.csp.vulnerabilities.table.filterOut": "除外", @@ -12279,24 +12246,12 @@ "xpack.csp.vulnerabilities.vulnerabilityOverviewTile.publishedDate": "公開日", "xpack.csp.vulnerabilitiesByResource.severityMap.tooltipTitle": "重要度マップ", "xpack.csp.vulnerability_dashboard.cspPageTemplate.pageTitle": "Cloud Native Vulnerability Management", - "xpack.csp.vulnerabilityByResourceTable.column.region": "地域", - "xpack.csp.vulnerabilityByResourceTable.column.resourceId": "リソースID", - "xpack.csp.vulnerabilityByResourceTable.column.resourceName": "リソース名", - "xpack.csp.vulnerabilityByResourceTable.column.severityMap": "重要度マップ", - "xpack.csp.vulnerabilityByResourceTable.column.vulnerabilities": "脆弱性", "xpack.csp.vulnerabilityDashboard.trendGraphChart.accountsDropDown.option.allTitle": "すべて", "xpack.csp.vulnerabilityDashboard.trendGraphChart.accountsDropDown.prepend.accountsTitle": "アカウント", "xpack.csp.vulnerabilityDashboard.trendGraphChart.trendBySeverityTitle": "重要度別傾向", "xpack.csp.vulnerabilityDashboard.viewAllButton.buttonTitle": "すべて表示", - "xpack.csp.vulnerabilityTable.column.fixVersion": "修正バージョン", - "xpack.csp.vulnerabilityTable.column.package": "パッケージ", - "xpack.csp.vulnerabilityTable.column.resourceId": "リソースID", - "xpack.csp.vulnerabilityTable.column.resourceName": "リソース名", - "xpack.csp.vulnerabilityTable.column.severity": "深刻度", "xpack.csp.vulnerabilityTable.column.sortAscending": "低 -> 重大", "xpack.csp.vulnerabilityTable.column.sortDescending": "重大 -> 低", - "xpack.csp.vulnerabilityTable.column.version": "バージョン", - "xpack.csp.vulnerabilityTable.column.vulnerability": "脆弱性", "xpack.csp.vulnerabilityTable.panel.buttonText": "すべての脆弱性を表示", "xpack.csp.vulnMgmtIntegration.awsOption.nameTitle": "Amazon Web Services", "xpack.csp.vulnMgmtIntegration.azureOption.nameTitle": "Azure", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 34f09ecbeadf1b..c39642226aaa24 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12011,13 +12011,7 @@ "xpack.csp.cloudPosturePage.kspmIntegration.packageNotInstalled.description": "使用我们的 {integrationFullName} (KSPM) 集成可在您的 Kubernetes 集群中检测安全配置错误。", "xpack.csp.complianceScoreBar.tooltipTitle": "{failed} 个失败和 {passed} 个通过的结果", "xpack.csp.eksIntegration.docsLink": "请参阅 {docs} 了解更多详情", - "xpack.csp.findings..bottomBarLabel": "这些是匹配您的搜索的前 {maxItems} 个结果,请优化搜索以查看其他结果。", "xpack.csp.findings.distributionBar.showingPageOfTotalLabel": "正在显示第 {pageStart}-{pageEnd} 个 {type}(共 {total} 个)", - "xpack.csp.findings.findingsTableCell.addFilterButton": "添加 {field} 筛选", - "xpack.csp.findings.findingsTableCell.addFilterButtonTooltip": "添加 {field} 筛选", - "xpack.csp.findings.findingsTableCell.addNegatedFilterButtonTooltip": "添加 {field} 作废筛选", - "xpack.csp.findings.findingsTableCell.addNegateFilterButton": "添加 {field} 作废筛选", - "xpack.csp.findings.resourceFindings.resourceFindingsPageTitle": "{resourceName} {hyphen} 结果", "xpack.csp.findingsFlyout.alerts.alertCount": "{alertCount, plural, other {# 个告警}}", "xpack.csp.findingsFlyout.alerts.detectionRuleCount": "{ruleCount, plural, other {# 个检测规则}}", "xpack.csp.noFindingsStates.indexTimeout.indexTimeoutDescription": "收集结果所需的时间长于预期。{docs}。", @@ -12025,15 +12019,7 @@ "xpack.csp.rules.rulesTable.showingPageOfTotalLabel": "正在显示 {pageSize} 个规则(共 {total, plural, other {# 个规则}})", "xpack.csp.subscriptionNotAllowed.promptDescription": "要使用这些云安全功能,您必须 {link}。", "xpack.csp.vulnerabilities.detectionRuleNamePrefix": "漏洞:{vulnerabilityId}", - "xpack.csp.vulnerabilities.resourceVulnerabilities.vulnerabilitiesPageTitle": "{resourceName} {hyphen} 漏洞", - "xpack.csp.vulnerabilities.totalVulnerabilities": "{total, plural, other {# 个漏洞}}", - "xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addFilterButton": "添加 {columnId} 筛选", - "xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addFilterButtonTooltip": "添加 {columnId} 筛选", - "xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addNegatedFilterButtonTooltip": "添加 {columnId} 作废筛选", - "xpack.csp.vulnerabilities.vulnerabilitiesTableCell.addNegateFilterButton": "添加 {columnId} 作废筛选", "xpack.csp.vulnerabilities.vulnerabilityOverviewTile.publishedDateText": "{date}", - "xpack.csp.vulnerabilitiesByResource.totalResources": "{total, plural, other {# 项资源}}", - "xpack.csp.vulnerabilitiesByResource.totalVulnerabilities": "{total, plural, other {# 个漏洞}}", "xpack.csp.awsIntegration.accessKeyIdLabel": "访问密钥 ID", "xpack.csp.awsIntegration.assumeRoleDescription": "IAM 角色 Amazon 资源名称 (ARN) 是您可在 AWS 帐户中创建的 IAM 身份。创建 IAM 角色时,用户可以定义该角色的权限。角色没有标准的长期凭据,如密码或访问密钥。", "xpack.csp.awsIntegration.assumeRoleLabel": "接管角色", @@ -12168,15 +12154,10 @@ "xpack.csp.emptyState.readDocsLink": "阅读文档", "xpack.csp.emptyState.resetFiltersButton": "重置筛选", "xpack.csp.emptyState.title": "没有任何结果匹配您的搜索条件", - "xpack.csp.expandColumnDescriptionLabel": "展开", - "xpack.csp.expandColumnNameLabel": "展开", "xpack.csp.findings.distributionBar.totalFailedLabel": "失败的结果", "xpack.csp.findings.distributionBar.totalPassedLabel": "通过的结果", "xpack.csp.findings.errorCallout.pageSearchErrorTitle": "检索搜索结果时遇到问题", "xpack.csp.findings.errorCallout.showErrorButtonLabel": "显示错误消息", - "xpack.csp.findings.findingsByResource.tableRowTypeLabel": "资源", - "xpack.csp.findings.findingsByResourceTable.cisSectionsColumnLabel": "CIS 部分", - "xpack.csp.findings.findingsByResourceTable.postureScoreColumnLabel": "态势分数", "xpack.csp.findings.findingsErrorToast.searchFailedTitle": "搜索失败", "xpack.csp.findings.findingsFlyout.jsonTabTitle": "JSON", "xpack.csp.findings.findingsFlyout.overviewTab.alertsTitle": "告警", @@ -12208,16 +12189,11 @@ "xpack.csp.findings.findingsFlyout.ruleTab.tagsTitle": "标签", "xpack.csp.findings.findingsFlyout.ruleTabTitle": "规则", "xpack.csp.findings.findingsFlyout.tableTabTitle": "表", - "xpack.csp.findings.findingsTable.findingsTableColumn.clusterIdColumnLabel": "属于", - "xpack.csp.findings.findingsTable.findingsTableColumn.clusterIdColumnTooltipLabel": "Kubernetes 集群 ID 或云帐户名称", "xpack.csp.findings.findingsTable.findingsTableColumn.lastCheckedColumnLabel": "上次检查时间", "xpack.csp.findings.findingsTable.findingsTableColumn.resourceIdColumnLabel": "资源 ID", - "xpack.csp.findings.findingsTable.findingsTableColumn.resourceIdColumnTooltipLabel": "定制 Elastic 资源 ID", "xpack.csp.findings.findingsTable.findingsTableColumn.resourceNameColumnLabel": "资源名称", "xpack.csp.findings.findingsTable.findingsTableColumn.resourceTypeColumnLabel": "资源类型", "xpack.csp.findings.findingsTable.findingsTableColumn.resultColumnLabel": "结果", - "xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnLabel": "适用基准", - "xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnTooltipLabel": "用于评估此资源的基准", "xpack.csp.findings.findingsTable.findingsTableColumn.ruleNameColumnLabel": "规则名称", "xpack.csp.findings.findingsTable.findingsTableColumn.ruleNumberColumnLabel": "规则编号", "xpack.csp.findings.findingsTable.findingsTableColumn.ruleSectionColumnLabel": "CIS 部分", @@ -12228,12 +12204,6 @@ "xpack.csp.findings.groupBySelector.groupByNoneLabel": "无", "xpack.csp.findings.groupBySelector.groupByResourceIdLabel": "资源", "xpack.csp.findings.latestFindings.tableRowTypeLabel": "结果", - "xpack.csp.findings.resourceFindings.backToResourcesPageButtonLabel": "返回到资源", - "xpack.csp.findings.resourceFindings.tableRowTypeLabel": "结果", - "xpack.csp.findings.resourceFindingsSharedValues.cloudAccountName": "云帐户名称", - "xpack.csp.findings.resourceFindingsSharedValues.clusterIdTitle": "集群 ID", - "xpack.csp.findings.resourceFindingsSharedValues.resourceIdTitle": "资源 ID", - "xpack.csp.findings.resourceFindingsSharedValues.resourceTypeTitle": "资源类型", "xpack.csp.findings.search.queryErrorToastMessage": "查询错误", "xpack.csp.findings.searchBar.searchPlaceholder": "搜索结果(例如,rule.section:“APM 服务器”)", "xpack.csp.findings.tabs.misconfigurations": "错误配置", @@ -12348,9 +12318,6 @@ "xpack.csp.vulnerabilities": "漏洞", "xpack.csp.vulnerabilities.flyoutTabs.fieldLabel": "字段", "xpack.csp.vulnerabilities.flyoutTabs.fieldValueLabel": "值", - "xpack.csp.vulnerabilities.resourceVulnerabilities.backToResourcesPageButtonLabel": "返回到资源", - "xpack.csp.vulnerabilities.resourceVulnerabilities.regionTitle": "地区", - "xpack.csp.vulnerabilities.resourceVulnerabilities.resourceIdTitle": "资源 ID", "xpack.csp.vulnerabilities.searchBar.placeholder": "搜索漏洞(例如,vulnerability.severity:“CRITICAL”)", "xpack.csp.vulnerabilities.table.filterIn": "筛选范围", "xpack.csp.vulnerabilities.table.filterOut": "筛除", @@ -12373,24 +12340,12 @@ "xpack.csp.vulnerabilities.vulnerabilityOverviewTile.publishedDate": "发布日期", "xpack.csp.vulnerabilitiesByResource.severityMap.tooltipTitle": "严重性映射", "xpack.csp.vulnerability_dashboard.cspPageTemplate.pageTitle": "云原生漏洞管理", - "xpack.csp.vulnerabilityByResourceTable.column.region": "地区", - "xpack.csp.vulnerabilityByResourceTable.column.resourceId": "资源 ID", - "xpack.csp.vulnerabilityByResourceTable.column.resourceName": "资源名称", - "xpack.csp.vulnerabilityByResourceTable.column.severityMap": "严重性映射", - "xpack.csp.vulnerabilityByResourceTable.column.vulnerabilities": "漏洞", "xpack.csp.vulnerabilityDashboard.trendGraphChart.accountsDropDown.option.allTitle": "全部", "xpack.csp.vulnerabilityDashboard.trendGraphChart.accountsDropDown.prepend.accountsTitle": "帐户", "xpack.csp.vulnerabilityDashboard.trendGraphChart.trendBySeverityTitle": "趋势(按严重性)", "xpack.csp.vulnerabilityDashboard.viewAllButton.buttonTitle": "查看全部", - "xpack.csp.vulnerabilityTable.column.fixVersion": "修复版本", - "xpack.csp.vulnerabilityTable.column.package": "软件包", - "xpack.csp.vulnerabilityTable.column.resourceId": "资源 ID", - "xpack.csp.vulnerabilityTable.column.resourceName": "资源名称", - "xpack.csp.vulnerabilityTable.column.severity": "严重性", "xpack.csp.vulnerabilityTable.column.sortAscending": "低 -> 严重", "xpack.csp.vulnerabilityTable.column.sortDescending": "严重 -> 低", - "xpack.csp.vulnerabilityTable.column.version": "版本", - "xpack.csp.vulnerabilityTable.column.vulnerability": "漏洞", "xpack.csp.vulnerabilityTable.panel.buttonText": "查看所有漏洞", "xpack.csp.vulnMgmtIntegration.awsOption.nameTitle": "Amazon Web Services", "xpack.csp.vulnMgmtIntegration.azureOption.nameTitle": "Azure", diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts index eb7ea675601548..5c42f0e9bf9aed 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts @@ -12,6 +12,10 @@ import type { FtrProviderContext } from '../ftr_provider_context'; // Defined in CSP plugin const FINDINGS_INDEX = 'logs-cloud_security_posture.findings-default'; const FINDINGS_LATEST_INDEX = 'logs-cloud_security_posture.findings_latest-default'; +export const VULNERABILITIES_INDEX_DEFAULT_NS = + 'logs-cloud_security_posture.vulnerabilities-default'; +export const LATEST_VULNERABILITIES_INDEX_DEFAULT_NS = + 'logs-cloud_security_posture.vulnerabilities_latest-default'; export function FindingsPageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); @@ -35,49 +39,49 @@ export function FindingsPageProvider({ getService, getPageObjects }: FtrProvider log.debug('CSP plugin is initialized'); }); + const deleteByQuery = async (index: string) => { + await es.deleteByQuery({ + index, + query: { + match_all: {}, + }, + ignore_unavailable: true, + refresh: true, + }); + }; + + const insertOperation = (index: string, findingsMock: Array>) => { + return findingsMock.flatMap((doc) => [{ index: { _index: index } }, doc]); + }; + const index = { + remove: () => + Promise.all([deleteByQuery(FINDINGS_INDEX), deleteByQuery(FINDINGS_LATEST_INDEX)]), + add: async (findingsMock: Array>) => { + await es.bulk({ + refresh: true, + operations: [ + ...insertOperation(FINDINGS_INDEX, findingsMock), + ...insertOperation(FINDINGS_LATEST_INDEX, findingsMock), + ], + }); + }, + }; + + const vulnerabilitiesIndex = { remove: () => Promise.all([ - es.deleteByQuery({ - index: FINDINGS_INDEX, - query: { - match_all: {}, - }, - ignore_unavailable: true, - refresh: true, - }), - es.deleteByQuery({ - index: FINDINGS_LATEST_INDEX, - query: { - match_all: {}, - }, - ignore_unavailable: true, - refresh: true, - }), + deleteByQuery(VULNERABILITIES_INDEX_DEFAULT_NS), + deleteByQuery(LATEST_VULNERABILITIES_INDEX_DEFAULT_NS), ]), add: async (findingsMock: Array>) => { - await Promise.all([ - ...findingsMock.map((finding) => - es.index({ - index: FINDINGS_INDEX, - body: { - ...finding, - '@timestamp': finding['@timestamp'] ?? new Date().toISOString(), - }, - refresh: true, - }) - ), - ...findingsMock.map((finding) => - es.index({ - index: FINDINGS_LATEST_INDEX, - body: { - ...finding, - '@timestamp': finding['@timestamp'] ?? new Date().toISOString(), - }, - refresh: true, - }) - ), - ]); + await es.bulk({ + refresh: true, + operations: [ + ...insertOperation(VULNERABILITIES_INDEX_DEFAULT_NS, findingsMock), + ...insertOperation(LATEST_VULNERABILITIES_INDEX_DEFAULT_NS, findingsMock), + ], + }); }, }; @@ -229,122 +233,15 @@ export function FindingsPageProvider({ getService, getPageObjects }: FtrProvider }, }); - const createTableObject = (tableTestSubject: string) => ({ - getElement() { - return testSubjects.find(tableTestSubject); - }, - - async getHeaders() { - const element = await this.getElement(); - return await element.findAllByCssSelector('thead tr :is(th,td)'); - }, - - async getColumnIndex(columnName: string) { - const headers = await this.getHeaders(); - const texts = await Promise.all(headers.map((header) => header.getVisibleText())); - const columnIndex = texts.findIndex((i) => i === columnName); - expect(columnIndex).to.be.greaterThan(-1); - return columnIndex + 1; - }, - - async getColumnHeaderCell(columnName: string) { - const headers = await this.getHeaders(); - const headerIndexes = await Promise.all(headers.map((header) => header.getVisibleText())); - const columnIndex = headerIndexes.findIndex((i) => i === columnName); - return headers[columnIndex]; - }, - - async getRowsCount() { - const element = await this.getElement(); - const rows = await element.findAllByCssSelector('tbody tr'); - return rows.length; - }, - - async getFindingsCount(type: 'passed' | 'failed') { - const element = await this.getElement(); - const items = await element.findAllByCssSelector(`span[data-test-subj="${type}_finding"]`); - return items.length; - }, - - async getRowIndexForValue(columnName: string, value: string) { - const values = await this.getColumnValues(columnName); - const rowIndex = values.indexOf(value); - expect(rowIndex).to.be.greaterThan(-1); - return rowIndex + 1; - }, - - async getFilterElementButton(rowIndex: number, columnIndex: number, negated = false) { - const tableElement = await this.getElement(); - const button = negated - ? 'findings_table_cell_add_negated_filter' - : 'findings_table_cell_add_filter'; - const selector = `tbody tr:nth-child(${rowIndex}) td:nth-child(${columnIndex}) button[data-test-subj="${button}"]`; - return tableElement.findByCssSelector(selector); - }, - - async addCellFilter(columnName: string, cellValue: string, negated = false) { - const columnIndex = await this.getColumnIndex(columnName); - const rowIndex = await this.getRowIndexForValue(columnName, cellValue); - const filterElement = await this.getFilterElementButton(rowIndex, columnIndex, negated); - await filterElement.click(); - }, - - async getColumnValues(columnName: string) { - const elementsWithNoFilterCell = ['CIS Section', '@timestamp']; - const tableElement = await this.getElement(); - const columnIndex = await this.getColumnIndex(columnName); - const selector = elementsWithNoFilterCell.includes(columnName) - ? `tbody tr td:nth-child(${columnIndex})` - : `tbody tr td:nth-child(${columnIndex}) div[data-test-subj="filter_cell_value"]`; - const columnCells = await tableElement.findAllByCssSelector(selector); - - return await Promise.all(columnCells.map((cell) => cell.getVisibleText())); - }, - - async hasColumnValue(columnName: string, value: string) { - const values = await this.getColumnValues(columnName); - return values.includes(value); - }, - - async toggleColumnSort(columnName: string, direction: 'asc' | 'desc') { - const element = await this.getColumnHeaderCell(columnName); - const currentSort = await element.getAttribute('aria-sort'); - if (currentSort === 'none') { - // a click is needed to focus on Eui column header - await element.click(); - - // default is ascending - if (direction === 'desc') { - const nonStaleElement = await this.getColumnHeaderCell(columnName); - await nonStaleElement.click(); - } - } - if ( - (currentSort === 'ascending' && direction === 'desc') || - (currentSort === 'descending' && direction === 'asc') - ) { - // Without getting the element again, the click throws an error (stale element reference) - const nonStaleElement = await this.getColumnHeaderCell(columnName); - await nonStaleElement.click(); - } - }, - - async openFlyoutAt(rowIndex: number) { - const table = await this.getElement(); - const flyoutButton = await table.findAllByTestSubject('findings_table_expand_column'); - await flyoutButton[rowIndex].click(); - }, - }); - const navigateToLatestFindingsPage = async () => { await PageObjects.common.navigateToUrl( 'securitySolution', // Defined in Security Solution plugin - 'cloud_security_posture/findings', + 'cloud_security_posture/findings/configurations', { shouldUseHashForSubUrl: false } ); }; - const navigateToVulnerabilities = async () => { + const navigateToLatestVulnerabilitiesPage = async () => { await PageObjects.common.navigateToUrl( 'securitySolution', // Defined in Security Solution plugin 'cloud_security_posture/findings/vulnerabilities', @@ -361,20 +258,8 @@ export function FindingsPageProvider({ getService, getPageObjects }: FtrProvider }; const latestFindingsTable = createDataTableObject('latest_findings_table'); - const resourceFindingsTable = createTableObject('resource_findings_table'); - const findingsByResourceTable = { - ...createTableObject('findings_by_resource_table'), - async clickResourceIdLink(resourceId: string, sectionName: string) { - const table = await this.getElement(); - const row = await table.findByCssSelector( - `[data-test-subj="findings_resource_table_row_${resourceId}/${sectionName}"]` - ); - const link = await row.findByCssSelector( - '[data-test-subj="findings_by_resource_table_resource_id_column"' - ); - await link.click(); - }, - }; + const latestVulnerabilitiesTable = createDataTableObject('latest_vulnerabilities_table'); + const notInstalledVulnerabilities = createNotInstalledObject('cnvm-integration-not-installed'); const notInstalledCSP = createNotInstalledObject('cloud_posture_page_package_not_installed'); @@ -463,14 +348,14 @@ export function FindingsPageProvider({ getService, getPageObjects }: FtrProvider return { navigateToLatestFindingsPage, - navigateToVulnerabilities, + navigateToLatestVulnerabilitiesPage, navigateToMisconfigurations, latestFindingsTable, - resourceFindingsTable, - findingsByResourceTable, + latestVulnerabilitiesTable, notInstalledVulnerabilities, notInstalledCSP, index, + vulnerabilitiesIndex, waitForPluginInitialized, distributionBar, vulnerabilityDataGrid, diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings_onboarding.ts b/x-pack/test/cloud_security_posture_functional/pages/findings_onboarding.ts index 4919e4102df871..765dc7fae1370b 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings_onboarding.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings_onboarding.ts @@ -28,7 +28,7 @@ export default ({ getPageObjects }: FtrProviderContext) => { }); it('clicking on the `No integrations installed` prompt action button - `install CNVM`: navigates to the CNVM integration installation page', async () => { - await findings.navigateToVulnerabilities(); + await findings.navigateToLatestVulnerabilitiesPage(); await PageObjects.header.waitUntilLoadingHasFinished(); const element = await notInstalledVulnerabilities.getElement(); expect(element).to.not.be(null); diff --git a/x-pack/test/cloud_security_posture_functional/pages/index.ts b/x-pack/test/cloud_security_posture_functional/pages/index.ts index 9da8cbbeeed541..f4039dc08466f5 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/index.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/index.ts @@ -18,5 +18,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./vulnerability_dashboard')); loadTestFile(require.resolve('./cis_integration')); loadTestFile(require.resolve('./findings_old_data')); + loadTestFile(require.resolve('./vulnerabilities')); + loadTestFile(require.resolve('./vulnerabilities_grouping')); }); } diff --git a/x-pack/test/cloud_security_posture_functional/pages/vulnerabilities.ts b/x-pack/test/cloud_security_posture_functional/pages/vulnerabilities.ts new file mode 100644 index 00000000000000..d882d1765f752d --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/pages/vulnerabilities.ts @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../ftr_provider_context'; +import { vulnerabilitiesLatestMock } from '../mocks/vulnerabilities_latest_mock'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const queryBar = getService('queryBar'); + const filterBar = getService('filterBar'); + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const pageObjects = getPageObjects(['common', 'findings', 'header']); + + const resourceName1 = 'name-ng-1-Node'; + const resourceName2 = 'othername-june12-8-8-0-1'; + + describe('Vulnerabilities Page - DataTable', function () { + this.tags(['cloud_security_posture_vulnerabilities']); + let findings: typeof pageObjects.findings; + let latestVulnerabilitiesTable: typeof findings.latestVulnerabilitiesTable; + + before(async () => { + findings = pageObjects.findings; + latestVulnerabilitiesTable = findings.latestVulnerabilitiesTable; + + // Before we start any test we must wait for cloud_security_posture plugin to complete its initialization + await findings.waitForPluginInitialized(); + + // Prepare mocked findings + await findings.vulnerabilitiesIndex.remove(); + await findings.vulnerabilitiesIndex.add(vulnerabilitiesLatestMock); + + await findings.navigateToLatestVulnerabilitiesPage(); + await retry.waitFor( + 'Findings table to be loaded', + async () => + (await latestVulnerabilitiesTable.getRowsCount()) === vulnerabilitiesLatestMock.length + ); + pageObjects.header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await findings.vulnerabilitiesIndex.remove(); + }); + + describe('SearchBar', () => { + it('add filter', async () => { + // Filter bar uses the field's customLabel in the DataView + await filterBar.addFilter({ + field: 'Resource Name', + operation: 'is', + value: resourceName1, + }); + + expect(await filterBar.hasFilter('resource.name', resourceName1)).to.be(true); + expect( + await latestVulnerabilitiesTable.hasColumnValue('resource.name', resourceName1) + ).to.be(true); + }); + + it('remove filter', async () => { + await filterBar.removeFilter('resource.name'); + + expect(await filterBar.hasFilter('resource.name', resourceName1)).to.be(false); + expect(await latestVulnerabilitiesTable.getRowsCount()).to.be( + vulnerabilitiesLatestMock.length + ); + }); + + it('set search query', async () => { + await queryBar.setQuery(resourceName1); + await queryBar.submitQuery(); + + expect( + await latestVulnerabilitiesTable.hasColumnValue('resource.name', resourceName1) + ).to.be(true); + expect( + await latestVulnerabilitiesTable.hasColumnValue('resource.name', resourceName2) + ).to.be(false); + + await queryBar.setQuery(''); + await queryBar.submitQuery(); + + expect(await latestVulnerabilitiesTable.getRowsCount()).to.be( + vulnerabilitiesLatestMock.length + ); + }); + }); + + describe('DataTable features', () => { + it('Edit data view field option is Enabled', async () => { + await latestVulnerabilitiesTable.toggleEditDataViewFieldsOption('vulnerability.id'); + expect(await testSubjects.find('gridEditFieldButton')).to.be.ok(); + await latestVulnerabilitiesTable.toggleEditDataViewFieldsOption('vulnerability.id'); + }); + }); + + describe('Vulnerabilities - Fields selector', () => { + const CSP_FIELDS_SELECTOR_MODAL = 'cloudSecurityFieldsSelectorModal'; + const CSP_FIELDS_SELECTOR_OPEN_BUTTON = 'cloudSecurityFieldsSelectorOpenButton'; + const CSP_FIELDS_SELECTOR_RESET_BUTTON = 'cloudSecurityFieldsSelectorResetButton'; + const CSP_FIELDS_SELECTOR_CLOSE_BUTTON = 'cloudSecurityFieldsSelectorCloseButton'; + + it('Add fields to the Vulnerabilities DataTable', async () => { + const fieldsButton = await testSubjects.find(CSP_FIELDS_SELECTOR_OPEN_BUTTON); + await fieldsButton.click(); + await testSubjects.existOrFail(CSP_FIELDS_SELECTOR_MODAL); + + const agentIdCheckbox = await testSubjects.find( + 'cloud-security-fields-selector-item-agent.id' + ); + await agentIdCheckbox.click(); + + const agentNameCheckbox = await testSubjects.find( + 'cloud-security-fields-selector-item-agent.name' + ); + await agentNameCheckbox.click(); + + await testSubjects.existOrFail('dataGridHeaderCell-agent.id'); + await testSubjects.existOrFail('dataGridHeaderCell-agent.name'); + + const closeFieldsButton = await testSubjects.find(CSP_FIELDS_SELECTOR_CLOSE_BUTTON); + await closeFieldsButton.click(); + await testSubjects.missingOrFail(CSP_FIELDS_SELECTOR_MODAL); + }); + + it('Remove fields from the Vulnerabilities DataTable', async () => { + const fieldsButton = await testSubjects.find(CSP_FIELDS_SELECTOR_OPEN_BUTTON); + await fieldsButton.click(); + + const agentIdCheckbox = await testSubjects.find( + 'cloud-security-fields-selector-item-agent.id' + ); + await agentIdCheckbox.click(); + + const agentNameCheckbox = await testSubjects.find( + 'cloud-security-fields-selector-item-agent.name' + ); + await agentNameCheckbox.click(); + + await testSubjects.missingOrFail('dataGridHeaderCell-agent.id'); + await testSubjects.missingOrFail('dataGridHeaderCell-agent.name'); + + const closeFieldsButton = await testSubjects.find(CSP_FIELDS_SELECTOR_CLOSE_BUTTON); + await closeFieldsButton.click(); + await testSubjects.missingOrFail(CSP_FIELDS_SELECTOR_MODAL); + }); + it('Reset fields to default', async () => { + const fieldsButton = await testSubjects.find(CSP_FIELDS_SELECTOR_OPEN_BUTTON); + await fieldsButton.click(); + + const agentIdCheckbox = await testSubjects.find( + 'cloud-security-fields-selector-item-agent.id' + ); + await agentIdCheckbox.click(); + + const agentNameCheckbox = await testSubjects.find( + 'cloud-security-fields-selector-item-agent.name' + ); + await agentNameCheckbox.click(); + + await testSubjects.existOrFail('dataGridHeaderCell-agent.id'); + await testSubjects.existOrFail('dataGridHeaderCell-agent.name'); + + const resetFieldsButton = await testSubjects.find(CSP_FIELDS_SELECTOR_RESET_BUTTON); + await resetFieldsButton.click(); + + await testSubjects.missingOrFail('dataGridHeaderCell-agent.id'); + await testSubjects.missingOrFail('dataGridHeaderCell-agent.name'); + + const closeFieldsButton = await testSubjects.find(CSP_FIELDS_SELECTOR_CLOSE_BUTTON); + await closeFieldsButton.click(); + await testSubjects.missingOrFail(CSP_FIELDS_SELECTOR_MODAL); + }); + }); + }); +} diff --git a/x-pack/test/cloud_security_posture_functional/pages/vulnerabilities_grouping.ts b/x-pack/test/cloud_security_posture_functional/pages/vulnerabilities_grouping.ts new file mode 100644 index 00000000000000..8e569d27b8a4dd --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/pages/vulnerabilities_grouping.ts @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { asyncForEach } from '@kbn/std'; +import type { FtrProviderContext } from '../ftr_provider_context'; +import { vulnerabilitiesLatestMock } from '../mocks/vulnerabilities_latest_mock'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const queryBar = getService('queryBar'); + const filterBar = getService('filterBar'); + const pageObjects = getPageObjects(['common', 'findings', 'header']); + + const resourceName1 = 'name-ng-1-Node'; + const resourceName2 = 'othername-june12-8-8-0-1'; + + describe('Vulnerabilities Page - Grouping', function () { + this.tags(['cloud_security_posture_findings_grouping']); + let findings: typeof pageObjects.findings; + + before(async () => { + findings = pageObjects.findings; + + // Before we start any test we must wait for cloud_security_posture plugin to complete its initialization + await findings.waitForPluginInitialized(); + + // Prepare mocked findings + await findings.vulnerabilitiesIndex.remove(); + await findings.vulnerabilitiesIndex.add(vulnerabilitiesLatestMock); + + await findings.navigateToLatestVulnerabilitiesPage(); + await pageObjects.header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + const groupSelector = await findings.groupSelector(); + await groupSelector.openDropDown(); + await groupSelector.setValue('None'); + await findings.vulnerabilitiesIndex.remove(); + }); + + describe('Default Grouping', async () => { + it('groups vulnerabilities by resource and sort by compliance score desc', async () => { + const groupSelector = await findings.groupSelector(); + await groupSelector.openDropDown(); + await groupSelector.setValue('Resource'); + + const grouping = await findings.findingsGrouping(); + + const resourceOrder = [ + { + resourceName: resourceName1, + resourceId: vulnerabilitiesLatestMock[0].resource.id, + findingsCount: '1', + }, + { + resourceName: resourceName2, + resourceId: vulnerabilitiesLatestMock[1].resource.id, + findingsCount: '1', + }, + ]; + + await asyncForEach( + resourceOrder, + async ({ resourceName, resourceId, findingsCount }, index) => { + const groupRow = await grouping.getRowAtIndex(index); + expect(await groupRow.getVisibleText()).to.contain(resourceName); + expect(await groupRow.getVisibleText()).to.contain(resourceId); + expect( + await ( + await groupRow.findByTestSubject('vulnerabilities_grouping_counter') + ).getVisibleText() + ).to.be(findingsCount); + } + ); + + const groupCount = await grouping.getGroupCount(); + expect(groupCount).to.be('2 groups'); + + const unitCount = await grouping.getUnitCount(); + expect(unitCount).to.be('2 vulnerabilities'); + }); + }); + describe('SearchBar', () => { + it('add filter', async () => { + // Filter bar uses the field's customLabel in the DataView + await filterBar.addFilter({ + field: 'Resource Name', + operation: 'is', + value: resourceName1, + }); + expect(await filterBar.hasFilter('resource.name', resourceName1)).to.be(true); + + const grouping = await findings.findingsGrouping(); + + const groupRow = await grouping.getRowAtIndex(0); + expect(await groupRow.getVisibleText()).to.contain(resourceName1); + + const groupCount = await grouping.getGroupCount(); + expect(groupCount).to.be('1 group'); + + const unitCount = await grouping.getUnitCount(); + expect(unitCount).to.be('1 vulnerability'); + }); + + it('remove filter', async () => { + await filterBar.removeFilter('resource.name'); + + expect(await filterBar.hasFilter('resource.name', resourceName1)).to.be(false); + + const grouping = await findings.findingsGrouping(); + const groupCount = await grouping.getGroupCount(); + expect(groupCount).to.be('2 groups'); + + const unitCount = await grouping.getUnitCount(); + expect(unitCount).to.be('2 vulnerabilities'); + }); + + it('set search query', async () => { + await queryBar.setQuery(resourceName1); + await queryBar.submitQuery(); + + const grouping = await findings.findingsGrouping(); + + const groupRow = await grouping.getRowAtIndex(0); + expect(await groupRow.getVisibleText()).to.contain(resourceName1); + + const groupCount = await grouping.getGroupCount(); + expect(groupCount).to.be('1 group'); + + const unitCount = await grouping.getUnitCount(); + expect(unitCount).to.be('1 vulnerability'); + + await queryBar.setQuery(''); + await queryBar.submitQuery(); + + expect(await grouping.getGroupCount()).to.be('2 groups'); + expect(await grouping.getUnitCount()).to.be('2 vulnerabilities'); + }); + }); + + describe('Group table', async () => { + it('shows vulnerabilities table when expanding', async () => { + const grouping = await findings.findingsGrouping(); + const firstRow = await grouping.getRowAtIndex(0); + await (await firstRow.findByCssSelector('button')).click(); + const latestFindingsTable = findings.createDataTableObject('latest_vulnerabilities_table'); + expect(await latestFindingsTable.getRowsCount()).to.be(1); + expect(await latestFindingsTable.hasColumnValue('resource.name', resourceName1)).to.be( + true + ); + }); + }); + }); +}