Skip to content

Commit

Permalink
[Cloud Posture] add resource findings table (#131334)
Browse files Browse the repository at this point in the history
  • Loading branch information
orouz authored May 3, 2022
1 parent 9740092 commit 687aad0
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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()],
[]
);

Expand Down Expand Up @@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
});
Expand All @@ -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}
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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 />
Expand All @@ -52,6 +76,11 @@ export const ResourceFindings = ({ dataView }: { dataView: DataView }) => {
/>
</PageTitle>
<EuiSpacer />
<ResourceFindingsTable
loading={resourceFindings.isLoading}
data={resourceFindings.data}
error={resourceFindings.error}
/>
</PageWrapper>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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),
}
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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>
),
},
];
Loading

0 comments on commit 687aad0

Please sign in to comment.