From 687aad03555021e5a8befb277e22a41223a187eb Mon Sep 17 00:00:00 2001 From: Or Ouziel Date: Tue, 3 May 2022 19:25:08 +0300 Subject: [PATCH] [Cloud Posture] add resource findings table (#131334) --- .../latest_findings/latest_findings_table.tsx | 84 ++----------------- .../findings_by_resource_container.tsx | 6 +- .../resource_findings_container.tsx | 29 +++++++ .../resource_findings_table.tsx | 32 +++++++ .../use_resource_findings.ts | 63 ++++++++++++++ .../pages/findings/layout/findings_layout.tsx | 83 +++++++++++++++++- .../public/pages/findings/translations.ts | 6 +- 7 files changed, 219 insertions(+), 84 deletions(-) create mode 100644 x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx create mode 100644 x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/use_resource_findings.ts diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx index 26a5b3d0ffe79..6a2bd1c129b50 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx @@ -7,23 +7,20 @@ import React, { useCallback, useMemo, useState } from 'react'; import { type Criteria, - EuiToolTip, - EuiTableFieldDataColumnType, EuiEmptyPrompt, EuiBasicTable, - PropsOf, EuiBasicTableProps, + EuiBasicTableColumn, } from '@elastic/eui'; -import moment from 'moment'; import { SortDirection } from '@kbn/data-plugin/common'; import { EuiTableActionsColumnType } from '@elastic/eui/src/components/basic_table/table_types'; import { extractErrorMessage } from '../../../../common/utils/helpers'; import * as TEST_SUBJECTS from '../test_subjects'; import * as TEXT from '../translations'; import type { CspFinding } from '../types'; -import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge'; import type { FindingsGroupByNoneQuery, CspFindingsResult } from './use_latest_findings'; import { FindingsRuleFlyout } from '../findings_flyout/findings_flyout'; +import { getExpandColumn, getFindingsColumns } from '../layout/findings_layout'; interface BaseFindingsTableProps extends FindingsGroupByNoneQuery { setQuery(query: Partial): void; @@ -42,62 +39,11 @@ const FindingsTableComponent = ({ }: FindingsTableProps) => { const [selectedFinding, setSelectedFinding] = useState(); - const columns: Array< - EuiTableFieldDataColumnType | EuiTableActionsColumnType - > = useMemo( - () => [ - { - width: '40px', - actions: [ - { - name: 'Expand', - description: 'Expand', - type: 'icon', - icon: 'expand', - onClick: (item) => setSelectedFinding(item), - }, - ], - }, - { - field: 'resource_id', - name: TEXT.RESOURCE_ID, - truncateText: true, - width: '15%', - sortable: true, - render: resourceFilenameRenderer, - }, - { - field: 'result.evaluation', - name: TEXT.RESULT, - width: '100px', - sortable: true, - render: resultEvaluationRenderer, - }, - { - field: 'rule.name', - name: TEXT.RULE, - sortable: true, - }, - { - field: 'cluster_id', - name: TEXT.SYSTEM_ID, - truncateText: true, - sortable: true, - }, - { - field: 'rule.section', - name: TEXT.CIS_SECTION, - sortable: true, - truncateText: true, - }, - { - field: '@timestamp', - name: TEXT.LAST_CHECKED, - truncateText: true, - sortable: true, - render: timestampRenderer, - }, - ], + const columns: [ + EuiTableActionsColumnType, + ...Array> + ] = useMemo( + () => [getExpandColumn({ onClick: setSelectedFinding }), ...getFindingsColumns()], [] ); @@ -188,20 +134,4 @@ const getEsSearchQueryFromEuiTableParams = ({ sort: sort ? [{ [sort.field]: SortDirection[sort.direction] }] : undefined, }); -const timestampRenderer = (timestamp: string) => ( - - {moment(timestamp).fromNow()} - -); - -const resourceFilenameRenderer = (filename: string) => ( - - {filename} - -); - -const resultEvaluationRenderer = (type: PropsOf['type']) => ( - -); - export const FindingsTable = React.memo(FindingsTableComponent); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx index 4b9ec040d4346..3dfbd477d4236 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx @@ -21,7 +21,7 @@ import { findingsNavigation } from '../../../common/navigation/constants'; import { useCspBreadcrumbs } from '../../../common/navigation/use_csp_breadcrumbs'; import { ResourceFindings } from './resource_findings/resource_findings_container'; -export const getDefaultQuery = (): FindingsBaseURLQuery => ({ +const getDefaultQuery = (): FindingsBaseURLQuery => ({ query: { language: 'kuery', query: '' }, filters: [], }); @@ -31,7 +31,7 @@ export const FindingsByResourceContainer = ({ dataView }: { dataView: DataView } } + render={() => } /> ); -const LatestFindingsByResourceContainer = ({ dataView }: { dataView: DataView }) => { +const LatestFindingsByResource = ({ dataView }: { dataView: DataView }) => { useCspBreadcrumbs([findingsNavigation.findings_by_resource]); const { urlQuery, setUrlQuery } = useUrlQuery(getDefaultQuery); const findingsGroupByResource = useFindingsByResource( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx index e693ea02cb13a..a48926b3653aa 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_container.tsx @@ -15,6 +15,17 @@ import * as TEST_SUBJECTS from '../../test_subjects'; import { PageWrapper, PageTitle, PageTitleText } from '../../layout/findings_layout'; import { useCspBreadcrumbs } from '../../../../common/navigation/use_csp_breadcrumbs'; import { findingsNavigation } from '../../../../common/navigation/constants'; +import { useResourceFindings } from './use_resource_findings'; +import { useUrlQuery } from '../../../../common/hooks/use_url_query'; +import type { FindingsBaseURLQuery } from '../../types'; +import { getBaseQuery } from '../../utils'; +import { ResourceFindingsTable } from './resource_findings_table'; +import { FindingsSearchBar } from '../../layout/findings_search_bar'; + +const getDefaultQuery = (): FindingsBaseURLQuery => ({ + query: { language: 'kuery', query: '' }, + filters: [], +}); const BackToResourcesButton = () => { return ( @@ -33,9 +44,22 @@ export const ResourceFindings = ({ dataView }: { dataView: DataView }) => { useCspBreadcrumbs([findingsNavigation.findings_default]); const { euiTheme } = useEuiTheme(); const params = useParams<{ resourceId: string }>(); + const { urlQuery, setUrlQuery } = useUrlQuery(getDefaultQuery); + + const resourceFindings = useResourceFindings({ + ...getBaseQuery({ dataView, filters: urlQuery.filters, query: urlQuery.query }), + resourceId: params.resourceId, + }); return (
+ @@ -52,6 +76,11 @@ export const ResourceFindings = ({ dataView }: { dataView: DataView }) => { /> +
); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx new file mode 100644 index 0000000000000..ec04d05109cdd --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx @@ -0,0 +1,32 @@ +/* + * 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 { EuiEmptyPrompt, EuiBasicTable } from '@elastic/eui'; +import { extractErrorMessage } from '../../../../../common/utils/helpers'; +import * as TEXT from '../../translations'; +import type { ResourceFindingsResult } from './use_resource_findings'; +import { getFindingsColumns } from '../../layout/findings_layout'; + +type FindingsGroupByResourceProps = ResourceFindingsResult; + +const columns = getFindingsColumns(); + +const ResourceFindingsTableComponent = ({ error, data, loading }: FindingsGroupByResourceProps) => { + if (!loading && !data?.page.length) + return {TEXT.NO_FINDINGS}} />; + + return ( + + ); +}; + +export const ResourceFindingsTable = React.memo(ResourceFindingsTableComponent); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/use_resource_findings.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/use_resource_findings.ts new file mode 100644 index 0000000000000..7123b80ef0228 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/use_resource_findings.ts @@ -0,0 +1,63 @@ +/* + * 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 'react-query'; +import { lastValueFrom } from 'rxjs'; +import { IEsSearchResponse } from '@kbn/data-plugin/common'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { useKibana } from '../../../../common/hooks/use_kibana'; +import { showErrorToast } from '../../latest_findings/use_latest_findings'; +import type { CspFinding, FindingsBaseEsQuery, FindingsQueryResult } from '../../types'; + +interface UseResourceFindingsOptions extends FindingsBaseEsQuery { + resourceId: string; +} + +export type ResourceFindingsResult = FindingsQueryResult< + ReturnType['data'] | undefined, + unknown +>; + +export const getResourceFindingsQuery = ({ + index, + query, + resourceId, +}: UseResourceFindingsOptions): estypes.SearchRequest => ({ + index, + body: { + query: { + ...query, + bool: { + ...query?.bool, + filter: [...(query?.bool?.filter || []), { term: { 'resource_id.keyword': resourceId } }], + }, + }, + }, +}); + +export const useResourceFindings = ({ index, query, resourceId }: UseResourceFindingsOptions) => { + const { + data, + notifications: { toasts }, + } = useKibana().services; + + return useQuery( + ['csp_resource_findings', { index, query, resourceId }], + () => + lastValueFrom>( + data.search.search({ + params: getResourceFindingsQuery({ index, query, resourceId }), + }) + ), + { + select: ({ rawResponse: { hits } }) => ({ + page: hits.hits.map((hit) => hit._source!), + total: hits.total as number, + }), + onError: (err) => showErrorToast(toasts, err), + } + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_layout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_layout.tsx index 337e1237d9287..ee1a00abc4901 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_layout.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_layout.tsx @@ -5,8 +5,20 @@ * 2.0. */ import React from 'react'; -import { EuiSpacer, EuiTitle, useEuiTheme } from '@elastic/eui'; +import { + EuiBasicTableColumn, + EuiSpacer, + EuiTableActionsColumnType, + EuiTitle, + EuiToolTip, + PropsOf, + useEuiTheme, +} from '@elastic/eui'; import { css } from '@emotion/react'; +import moment from 'moment'; +import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge'; +import * as TEXT from '../translations'; +import { CspFinding } from '../types'; export const PageWrapper: React.FC = ({ children }) => { const { euiTheme } = useEuiTheme(); @@ -31,3 +43,72 @@ export const PageTitle: React.FC = ({ children }) => ( ); export const PageTitleText = ({ title }: { title: React.ReactNode }) =>

{title}

; + +export const getExpandColumn = ({ + onClick, +}: { + onClick(item: T): void; +}): EuiTableActionsColumnType => ({ + width: '40px', + actions: [ + { + name: 'Expand', + description: 'Expand', + type: 'icon', + icon: 'expand', + onClick, + }, + ], +}); + +export const getFindingsColumns = (): Array> => [ + { + field: 'resource_id', + name: TEXT.RESOURCE_ID, + truncateText: true, + width: '15%', + sortable: true, + render: (filename: string) => ( + + {filename} + + ), + }, + { + field: 'result.evaluation', + name: TEXT.RESULT, + width: '100px', + sortable: true, + render: (type: PropsOf['type']) => ( + + ), + }, + { + field: 'rule.name', + name: TEXT.RULE, + sortable: true, + }, + { + field: 'cluster_id', + name: TEXT.CLUSTER_ID, + truncateText: true, + sortable: true, + }, + { + field: 'rule.section', + name: TEXT.CIS_SECTION, + sortable: true, + truncateText: true, + }, + { + field: '@timestamp', + name: TEXT.LAST_CHECKED, + truncateText: true, + sortable: true, + render: (timestamp: number) => ( + + {moment(timestamp).fromNow()} + + ), + }, +]; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts index 4256f3219b3f6..53d4b8a86e5c0 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts @@ -64,10 +64,10 @@ export const RESULT = i18n.translate( } ); -export const SYSTEM_ID = i18n.translate( - 'xpack.csp.findings.findingsTable.findingsTableColumn.systemIdColumnLabel', +export const CLUSTER_ID = i18n.translate( + 'xpack.csp.findings.findingsTable.findingsTableColumn.clusterIdColumnLabel', { - defaultMessage: 'System ID', + defaultMessage: 'Cluster ID', } );