mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Cloud Posture] add resource findings table (#131334)
This commit is contained in:
parent
9740092ace
commit
687aad0355
7 changed files with 219 additions and 84 deletions
|
@ -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<FindingsGroupByNoneQuery>): void;
|
||||
|
@ -42,62 +39,11 @@ const FindingsTableComponent = ({
|
|||
}: FindingsTableProps) => {
|
||||
const [selectedFinding, setSelectedFinding] = useState<CspFinding>();
|
||||
|
||||
const columns: Array<
|
||||
EuiTableFieldDataColumnType<CspFinding> | EuiTableActionsColumnType<CspFinding>
|
||||
> = 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<CspFinding>,
|
||||
...Array<EuiBasicTableColumn<CspFinding>>
|
||||
] = useMemo(
|
||||
() => [getExpandColumn<CspFinding>({ onClick: setSelectedFinding }), ...getFindingsColumns()],
|
||||
[]
|
||||
);
|
||||
|
||||
|
@ -188,20 +134,4 @@ const getEsSearchQueryFromEuiTableParams = ({
|
|||
sort: sort ? [{ [sort.field]: SortDirection[sort.direction] }] : undefined,
|
||||
});
|
||||
|
||||
const timestampRenderer = (timestamp: string) => (
|
||||
<EuiToolTip position="top" content={timestamp}>
|
||||
<span>{moment(timestamp).fromNow()}</span>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
||||
const resourceFilenameRenderer = (filename: string) => (
|
||||
<EuiToolTip position="top" content={filename}>
|
||||
<span>{filename}</span>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
||||
const resultEvaluationRenderer = (type: PropsOf<typeof CspEvaluationBadge>['type']) => (
|
||||
<CspEvaluationBadge type={type} />
|
||||
);
|
||||
|
||||
export const FindingsTable = React.memo(FindingsTableComponent);
|
||||
|
|
|
@ -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 }
|
|||
<Route
|
||||
exact
|
||||
path={findingsNavigation.findings_by_resource.path}
|
||||
render={() => <LatestFindingsByResourceContainer dataView={dataView} />}
|
||||
render={() => <LatestFindingsByResource dataView={dataView} />}
|
||||
/>
|
||||
<Route
|
||||
path={findingsNavigation.resource_findings.path}
|
||||
|
@ -40,7 +40,7 @@ export const FindingsByResourceContainer = ({ dataView }: { dataView: DataView }
|
|||
</Switch>
|
||||
);
|
||||
|
||||
const LatestFindingsByResourceContainer = ({ dataView }: { dataView: DataView }) => {
|
||||
const LatestFindingsByResource = ({ dataView }: { dataView: DataView }) => {
|
||||
useCspBreadcrumbs([findingsNavigation.findings_by_resource]);
|
||||
const { urlQuery, setUrlQuery } = useUrlQuery(getDefaultQuery);
|
||||
const findingsGroupByResource = useFindingsByResource(
|
||||
|
|
|
@ -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 (
|
||||
<div data-test-subj={TEST_SUBJECTS.FINDINGS_CONTAINER}>
|
||||
<FindingsSearchBar
|
||||
dataView={dataView}
|
||||
setQuery={setUrlQuery}
|
||||
query={urlQuery.query}
|
||||
filters={urlQuery.filters}
|
||||
loading={resourceFindings.isLoading}
|
||||
/>
|
||||
<PageWrapper>
|
||||
<PageTitle>
|
||||
<BackToResourcesButton />
|
||||
|
@ -52,6 +76,11 @@ export const ResourceFindings = ({ dataView }: { dataView: DataView }) => {
|
|||
/>
|
||||
</PageTitle>
|
||||
<EuiSpacer />
|
||||
<ResourceFindingsTable
|
||||
loading={resourceFindings.isLoading}
|
||||
data={resourceFindings.data}
|
||||
error={resourceFindings.error}
|
||||
/>
|
||||
</PageWrapper>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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 <EuiEmptyPrompt iconType="logoKibana" title={<h2>{TEXT.NO_FINDINGS}</h2>} />;
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
loading={loading}
|
||||
error={error ? extractErrorMessage(error) : undefined}
|
||||
items={data?.page || []}
|
||||
columns={columns}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ResourceFindingsTable = React.memo(ResourceFindingsTableComponent);
|
|
@ -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<typeof useResourceFindings>['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<IEsSearchResponse<CspFinding>>(
|
||||
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),
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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 }) => <h2>{title}</h2>;
|
||||
|
||||
export const getExpandColumn = <T extends unknown>({
|
||||
onClick,
|
||||
}: {
|
||||
onClick(item: T): void;
|
||||
}): EuiTableActionsColumnType<T> => ({
|
||||
width: '40px',
|
||||
actions: [
|
||||
{
|
||||
name: 'Expand',
|
||||
description: 'Expand',
|
||||
type: 'icon',
|
||||
icon: 'expand',
|
||||
onClick,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const getFindingsColumns = (): Array<EuiBasicTableColumn<CspFinding>> => [
|
||||
{
|
||||
field: 'resource_id',
|
||||
name: TEXT.RESOURCE_ID,
|
||||
truncateText: true,
|
||||
width: '15%',
|
||||
sortable: true,
|
||||
render: (filename: string) => (
|
||||
<EuiToolTip position="top" content={filename}>
|
||||
<span>{filename}</span>
|
||||
</EuiToolTip>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'result.evaluation',
|
||||
name: TEXT.RESULT,
|
||||
width: '100px',
|
||||
sortable: true,
|
||||
render: (type: PropsOf<typeof CspEvaluationBadge>['type']) => (
|
||||
<CspEvaluationBadge type={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) => (
|
||||
<EuiToolTip position="top" content={timestamp}>
|
||||
<span>{moment(timestamp).fromNow()}</span>
|
||||
</EuiToolTip>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue