mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Cloud Security]Updating rendering method for Misconfiguration Findings Flyout (#216116)
## Summary https://github.com/user-attachments/assets/d83e79af-f369-48ab-b7cb-1853086e7ec1 As a part of implementing new Findings Flyout, we are updating the way we render Findings Flyout in Findings page. This PR addresses that by using Expandable Flyout API hooks to handle which Flyout to render, previously we just render the flyout directly without using hooks --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Paulo Silva <paulo.henrique@elastic.co>
This commit is contained in:
parent
da5cfd6f32
commit
0ff53f8cc1
25 changed files with 805 additions and 185 deletions
|
@ -15346,7 +15346,6 @@
|
|||
"xpack.csp.findings.findingsFlyout.overviewTab.ruleTagsTitle": "Balises de règle",
|
||||
"xpack.csp.findings.findingsFlyout.overviewTab.vendorTitle": "Fournisseur",
|
||||
"xpack.csp.findings.findingsFlyout.overviewTabTitle": "Aperçu",
|
||||
"xpack.csp.findings.findingsFlyout.paginationLabel": "Navigation de recherche",
|
||||
"xpack.csp.findings.findingsFlyout.ruleNameTabField.ruleNameTooltip": "Gérer la règle",
|
||||
"xpack.csp.findings.findingsFlyout.ruleTab.AlertsTitle": "Alertes",
|
||||
"xpack.csp.findings.findingsFlyout.ruleTab.auditTitle": "Audit",
|
||||
|
|
|
@ -15328,7 +15328,6 @@
|
|||
"xpack.csp.findings.findingsFlyout.overviewTab.ruleTagsTitle": "ルールタグ",
|
||||
"xpack.csp.findings.findingsFlyout.overviewTab.vendorTitle": "ベンダー",
|
||||
"xpack.csp.findings.findingsFlyout.overviewTabTitle": "概要",
|
||||
"xpack.csp.findings.findingsFlyout.paginationLabel": "ナビゲーションを検索中",
|
||||
"xpack.csp.findings.findingsFlyout.ruleNameTabField.ruleNameTooltip": "ルールの管理",
|
||||
"xpack.csp.findings.findingsFlyout.ruleTab.AlertsTitle": "アラート",
|
||||
"xpack.csp.findings.findingsFlyout.ruleTab.auditTitle": "監査",
|
||||
|
|
|
@ -15362,7 +15362,6 @@
|
|||
"xpack.csp.findings.findingsFlyout.overviewTab.ruleTagsTitle": "规则标签",
|
||||
"xpack.csp.findings.findingsFlyout.overviewTab.vendorTitle": "向量",
|
||||
"xpack.csp.findings.findingsFlyout.overviewTabTitle": "概览",
|
||||
"xpack.csp.findings.findingsFlyout.paginationLabel": "正在查找导航",
|
||||
"xpack.csp.findings.findingsFlyout.ruleNameTabField.ruleNameTooltip": "管理规则",
|
||||
"xpack.csp.findings.findingsFlyout.ruleTab.AlertsTitle": "告警",
|
||||
"xpack.csp.findings.findingsFlyout.ruleTab.auditTitle": "审计",
|
||||
|
|
|
@ -20,3 +20,4 @@ export { getSeverityText } from './src/utils/get_vulnerability_text';
|
|||
export { getVulnerabilityStats, hasVulnerabilitiesData } from './src/utils/vulnerability_helpers';
|
||||
export { CVSScoreBadge, SeverityStatusBadge } from './src/components/vulnerability_badges';
|
||||
export { getNormalizedSeverity } from './src/utils/get_normalized_severity';
|
||||
export { createMisconfigurationFindingsQuery } from './src/utils/findings_query_builders';
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { CDR_MISCONFIGURATIONS_INDEX_PATTERN } from '@kbn/cloud-security-posture-common';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { showErrorToast } from '../..';
|
||||
import type {
|
||||
CspClientPluginStartDeps,
|
||||
LatestFindingsRequest,
|
||||
LatestFindingsResponse,
|
||||
UseCspOptions,
|
||||
} from '../types';
|
||||
import { buildFindingsQueryWithFilters } from '../utils/findings_query_builders';
|
||||
|
||||
const GET_MISCONFIGURATIONS_SOURCE_FIELDS = [
|
||||
'result.*',
|
||||
'rule.*',
|
||||
'resource.*',
|
||||
'@timestamp',
|
||||
'observer',
|
||||
'data_stream.*',
|
||||
];
|
||||
|
||||
export const buildGetMisconfigurationsFindingsQuery = ({ query }: UseCspOptions) => {
|
||||
return {
|
||||
index: CDR_MISCONFIGURATIONS_INDEX_PATTERN,
|
||||
size: 1,
|
||||
ignore_unavailable: true,
|
||||
query: buildFindingsQueryWithFilters(query),
|
||||
_source: GET_MISCONFIGURATIONS_SOURCE_FIELDS,
|
||||
};
|
||||
};
|
||||
|
||||
export const useMisconfigurationFinding = (options: UseCspOptions) => {
|
||||
const {
|
||||
data,
|
||||
notifications: { toasts },
|
||||
} = useKibana<CoreStart & CspClientPluginStartDeps>().services;
|
||||
|
||||
return useQuery(
|
||||
['csp_misconfiguration_findings', { params: options }],
|
||||
async () => {
|
||||
const {
|
||||
rawResponse: { hits, aggregations },
|
||||
} = await lastValueFrom(
|
||||
data.search.search<LatestFindingsRequest, LatestFindingsResponse>({
|
||||
params: buildGetMisconfigurationsFindingsQuery(
|
||||
options
|
||||
) as LatestFindingsRequest['params'],
|
||||
})
|
||||
);
|
||||
if (!aggregations && options.ignore_unavailable === false)
|
||||
throw new Error('expected aggregations to be defined');
|
||||
return {
|
||||
result: hits,
|
||||
};
|
||||
},
|
||||
{
|
||||
enabled: options.enabled,
|
||||
keepPreviousData: false,
|
||||
onError: (err: Error) => showErrorToast(toasts, err),
|
||||
}
|
||||
);
|
||||
};
|
|
@ -15,7 +15,7 @@ import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin
|
|||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { ToastsStart } from '@kbn/core/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
||||
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
|
||||
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
|
||||
import type { DiscoverStart } from '@kbn/discover-plugin/public';
|
||||
import type { FleetStart } from '@kbn/fleet-plugin/public';
|
||||
|
@ -64,7 +64,7 @@ export interface CspBaseEsQuery {
|
|||
}
|
||||
|
||||
export interface UseCspOptions extends CspBaseEsQuery {
|
||||
sort: Array<{
|
||||
sort?: Array<{
|
||||
[key: string]: string;
|
||||
}>;
|
||||
enabled: boolean;
|
||||
|
@ -80,3 +80,12 @@ export type LatestFindingsResponse = IKibanaSearchResponse<
|
|||
export interface FindingsAggs {
|
||||
count: estypes.AggregationsMultiBucketAggregateBase<estypes.AggregationsStringRareTermsBucketKeys>;
|
||||
}
|
||||
|
||||
export interface FindingMisconfigurationFlyoutProps extends Record<string, unknown> {
|
||||
ruleId: string;
|
||||
resourceId: string;
|
||||
}
|
||||
export interface FindingsMisconfigurationPanelExpandableFlyoutProps extends FlyoutPanelProps {
|
||||
key: 'findings-misconfiguration-panel';
|
||||
params: FindingMisconfigurationFlyoutProps;
|
||||
}
|
||||
|
|
|
@ -192,7 +192,7 @@ export const getVulnerabilitiesQuery = ({ query, sort }: UseCspOptions, isPrevie
|
|||
sort,
|
||||
});
|
||||
|
||||
const buildVulnerabilityFindingsQueryWithFilters = (query: UseCspOptions['query']) => {
|
||||
export const buildVulnerabilityFindingsQueryWithFilters = (query: UseCspOptions['query']) => {
|
||||
return {
|
||||
...query,
|
||||
bool: {
|
||||
|
@ -211,3 +211,32 @@ const buildVulnerabilityFindingsQueryWithFilters = (query: UseCspOptions['query'
|
|||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const buildFindingsQueryWithFilters = (query: UseCspOptions['query']) => {
|
||||
return {
|
||||
...query,
|
||||
bool: {
|
||||
...query?.bool,
|
||||
filter: [...(query?.bool?.filter ?? [])],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const createMisconfigurationFindingsQuery = (resourceId?: string, ruleId?: string) => {
|
||||
return {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
'rule.id': ruleId,
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'resource.id': resourceId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -36,5 +36,6 @@
|
|||
"@kbn/i18n-react",
|
||||
"@kbn/rison",
|
||||
"@kbn/core-lifecycle-browser",
|
||||
"@kbn/expandable-flyout",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { useContext, useCallback, useState } from 'react';
|
||||
import { CspFinding } from '@kbn/cloud-security-posture-common';
|
||||
import { DataTableRecord } from '@kbn/discover-utils';
|
||||
import { SecuritySolutionContext } from '../../application/security_solution_context';
|
||||
|
||||
export const useExpandableFlyoutCsp = () => {
|
||||
const [expandedDoc, setExpandedDoc] = useState<DataTableRecord | undefined>(undefined);
|
||||
const securitySolutionContext = useContext(SecuritySolutionContext);
|
||||
|
||||
const setFlyoutCloseCallback = useCallback(
|
||||
(onChange: any) => {
|
||||
// Check if the context and required methods exist
|
||||
if (securitySolutionContext && securitySolutionContext.useOnExpandableFlyoutClose) {
|
||||
securitySolutionContext.useOnExpandableFlyoutClose({
|
||||
callback: () => onChange(undefined),
|
||||
});
|
||||
}
|
||||
},
|
||||
[securitySolutionContext]
|
||||
);
|
||||
|
||||
if (!securitySolutionContext || !securitySolutionContext.useExpandableFlyoutApi)
|
||||
return { onExpandDocClick: null };
|
||||
|
||||
const { openFlyout, closeFlyout } = securitySolutionContext.useExpandableFlyoutApi();
|
||||
|
||||
setFlyoutCloseCallback(setExpandedDoc);
|
||||
|
||||
const onExpandDocClick = (record?: DataTableRecord | undefined) => {
|
||||
if (record) {
|
||||
const finding = record?.raw?._source as unknown as CspFinding;
|
||||
setExpandedDoc(record);
|
||||
openFlyout({
|
||||
right: {
|
||||
id: 'findings-misconfiguration-panel',
|
||||
params: {
|
||||
resourceId: finding.resource.id,
|
||||
ruleId: finding.rule.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
closeFlyout();
|
||||
setExpandedDoc(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
return { expandedDoc, setExpandedDoc, onExpandDocClick };
|
||||
};
|
|
@ -9,6 +9,11 @@ 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';
|
||||
import { useExpandableFlyoutCsp } from '../../common/hooks/use_expandable_flyout_csp';
|
||||
|
||||
jest.mock('../../common/hooks/use_expandable_flyout_csp', () => ({
|
||||
useExpandableFlyoutCsp: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockDataView = {
|
||||
fields: {
|
||||
|
@ -66,7 +71,7 @@ const renderDataTable = (props: Partial<CloudSecurityDataTableProps> = {}) => {
|
|||
defaultColumns: mockDefaultColumns,
|
||||
rows: props.rows || mockRows,
|
||||
total: 0,
|
||||
flyoutComponent: () => <></>,
|
||||
onOpenFlyoutCallback: () => <></>,
|
||||
cloudPostureDataTable: mockCloudPostureDataTable,
|
||||
loadMore: jest.fn(),
|
||||
createRuleFn: jest.fn(),
|
||||
|
@ -85,6 +90,9 @@ const renderDataTable = (props: Partial<CloudSecurityDataTableProps> = {}) => {
|
|||
};
|
||||
|
||||
describe('CloudSecurityDataTable', () => {
|
||||
(useExpandableFlyoutCsp as jest.Mock).mockReturnValue({
|
||||
onExpandDocClick: jest.fn(),
|
||||
});
|
||||
it('renders loading state', () => {
|
||||
const { getByTestId } = renderDataTable({ isLoading: true, rows: [] });
|
||||
expect(getByTestId('unifiedDataTableLoading')).toBeInTheDocument();
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
DataGridDensity,
|
||||
|
@ -36,6 +36,7 @@ import { useStyles } from './use_styles';
|
|||
import { AdditionalControls } from './additional_controls';
|
||||
import { useDataViewContext } from '../../common/contexts/data_view_context';
|
||||
import { TakeAction } from '../take_action';
|
||||
import { useExpandableFlyoutCsp } from '../../common/hooks/use_expandable_flyout_csp';
|
||||
|
||||
export interface CloudSecurityDefaultColumn {
|
||||
id: string;
|
||||
|
@ -61,7 +62,7 @@ export interface CloudSecurityDataTableProps {
|
|||
* This is the component that will be rendered in the flyout when a row is expanded.
|
||||
* This component will receive the row data and a function to close the flyout.
|
||||
*/
|
||||
flyoutComponent: (hit: DataTableRecord, onCloseFlyout: () => void) => JSX.Element;
|
||||
onOpenFlyoutCallback: () => JSX.Element;
|
||||
/**
|
||||
* This is the object that contains all the data and functions from the useCloudPostureDataTable hook.
|
||||
* This is also used to manage the table state from the parent component.
|
||||
|
@ -107,7 +108,7 @@ export const CloudSecurityDataTable = ({
|
|||
defaultColumns,
|
||||
rows,
|
||||
total,
|
||||
flyoutComponent,
|
||||
onOpenFlyoutCallback,
|
||||
cloudPostureDataTable,
|
||||
loadMore,
|
||||
title,
|
||||
|
@ -161,14 +162,6 @@ export const CloudSecurityDataTable = ({
|
|||
};
|
||||
}, [persistedSettings, columnHeaders]);
|
||||
|
||||
const { dataView, dataViewIsRefetching } = useDataViewContext();
|
||||
|
||||
const [expandedDoc, setExpandedDoc] = useState<DataTableRecord | undefined>(undefined);
|
||||
|
||||
const renderDocumentView = (hit: DataTableRecord) =>
|
||||
flyoutComponent(hit, () => setExpandedDoc(undefined));
|
||||
|
||||
// services needed for unified-data-table package
|
||||
const {
|
||||
uiSettings,
|
||||
uiActions,
|
||||
|
@ -182,19 +175,8 @@ export const CloudSecurityDataTable = ({
|
|||
} = useKibana().services;
|
||||
|
||||
const styles = useStyles();
|
||||
|
||||
const { dataView, dataViewIsRefetching } = useDataViewContext();
|
||||
const { capabilities } = application;
|
||||
const { filterManager } = data.query;
|
||||
|
||||
const services = {
|
||||
theme,
|
||||
fieldFormats,
|
||||
uiSettings,
|
||||
toastNotifications,
|
||||
storage,
|
||||
data,
|
||||
};
|
||||
|
||||
const {
|
||||
columns: currentColumns,
|
||||
onSetColumns,
|
||||
|
@ -236,6 +218,8 @@ export const CloudSecurityDataTable = ({
|
|||
};
|
||||
}, [pageSize, height, filters?.length, hasDistributionBar]);
|
||||
|
||||
const { filterManager } = data.query;
|
||||
|
||||
const onAddFilter: AddFieldFilterHandler | undefined = useMemo(
|
||||
() =>
|
||||
filterManager && dataView
|
||||
|
@ -255,6 +239,27 @@ export const CloudSecurityDataTable = ({
|
|||
: undefined,
|
||||
[dataView, filterManager, setUrlQuery]
|
||||
);
|
||||
const externalCustomRenderers = useMemo(() => {
|
||||
if (!customCellRenderer) {
|
||||
return undefined;
|
||||
}
|
||||
return customCellRenderer(rows);
|
||||
}, [customCellRenderer, rows]);
|
||||
|
||||
const { expandedDoc, onExpandDocClick } = useExpandableFlyoutCsp();
|
||||
|
||||
if (!onExpandDocClick) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const services = {
|
||||
theme,
|
||||
fieldFormats,
|
||||
uiSettings,
|
||||
toastNotifications,
|
||||
storage,
|
||||
data,
|
||||
};
|
||||
|
||||
const onResize = (colSettings: { columnId: string; width: number | undefined }) => {
|
||||
const grid = persistedSettings || {};
|
||||
|
@ -266,13 +271,6 @@ export const CloudSecurityDataTable = ({
|
|||
setPersistedSettings(newGrid);
|
||||
};
|
||||
|
||||
const externalCustomRenderers = useMemo(() => {
|
||||
if (!customCellRenderer) {
|
||||
return undefined;
|
||||
}
|
||||
return customCellRenderer(rows);
|
||||
}, [customCellRenderer, rows]);
|
||||
|
||||
const onResetColumns = () => {
|
||||
setColumns(defaultColumns.map((c) => c.id));
|
||||
};
|
||||
|
@ -341,8 +339,8 @@ export const CloudSecurityDataTable = ({
|
|||
onSort={onSort}
|
||||
rows={rows}
|
||||
sampleSizeState={MAX_FINDINGS_TO_LOAD}
|
||||
setExpandedDoc={setExpandedDoc}
|
||||
renderDocumentView={renderDocumentView}
|
||||
setExpandedDoc={onExpandDocClick}
|
||||
renderDocumentView={onOpenFlyoutCallback}
|
||||
sort={sort}
|
||||
rowsPerPageState={pageSize}
|
||||
totalHits={total}
|
||||
|
|
|
@ -0,0 +1,367 @@
|
|||
/*
|
||||
* 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, { useState, useMemo } from 'react';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
DataGridDensity,
|
||||
UnifiedDataTableSettings,
|
||||
UnifiedDataTableSettingsColumn,
|
||||
useColumns,
|
||||
} from '@kbn/unified-data-table';
|
||||
import { UnifiedDataTable, DataLoadingState } from '@kbn/unified-data-table';
|
||||
import { CellActionsProvider } from '@kbn/cell-actions';
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { SHOW_MULTIFIELDS, SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils';
|
||||
import { DataTableRecord } from '@kbn/discover-utils/types';
|
||||
import {
|
||||
EuiDataGridCellValueElementProps,
|
||||
EuiDataGridControlColumn,
|
||||
EuiDataGridStyle,
|
||||
EuiProgress,
|
||||
} from '@elastic/eui';
|
||||
import { AddFieldFilterHandler } from '@kbn/unified-field-list';
|
||||
import { generateFilters } from '@kbn/data-plugin/public';
|
||||
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import { MAX_FINDINGS_TO_LOAD } from '@kbn/cloud-security-posture-common';
|
||||
import type { RuleResponse } from '@kbn/cloud-security-posture-common';
|
||||
import { useKibana } from '../../common/hooks/use_kibana';
|
||||
import { CloudPostureDataTableResult } from '../../common/hooks/use_cloud_posture_data_table';
|
||||
import { EmptyState } from '../empty_state';
|
||||
import { useStyles } from './use_styles';
|
||||
import { AdditionalControls } from './additional_controls';
|
||||
import { useDataViewContext } from '../../common/contexts/data_view_context';
|
||||
import { TakeAction } from '../take_action';
|
||||
|
||||
export interface CloudSecurityDefaultColumn {
|
||||
id: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
const gridStyle: EuiDataGridStyle = {
|
||||
border: 'horizontal',
|
||||
cellPadding: 'l',
|
||||
stripes: false,
|
||||
header: 'underline',
|
||||
};
|
||||
|
||||
// Hide Checkbox, enable open details Flyout
|
||||
const controlColumnIds = ['openDetails'];
|
||||
|
||||
export interface CloudSecurityDataTableProps {
|
||||
isLoading: boolean;
|
||||
defaultColumns: CloudSecurityDefaultColumn[];
|
||||
rows: DataTableRecord[];
|
||||
total: number;
|
||||
/**
|
||||
* This is the component that will be rendered in the flyout when a row is expanded.
|
||||
* This component will receive the row data and a function to close the flyout.
|
||||
*/
|
||||
flyoutComponent: (hit: DataTableRecord, onCloseFlyout: () => void) => JSX.Element;
|
||||
/**
|
||||
* This is the object that contains all the data and functions from the useCloudPostureDataTable hook.
|
||||
* This is also used to manage the table state from the parent component.
|
||||
*/
|
||||
cloudPostureDataTable: CloudPostureDataTableResult;
|
||||
title: string;
|
||||
/**
|
||||
* This is a function that returns a map of column ids to custom cell renderers.
|
||||
* This is useful for rendering custom components for cells in the table.
|
||||
*/
|
||||
customCellRenderer?: (rows: DataTableRecord[]) => {
|
||||
[key: string]: (props: EuiDataGridCellValueElementProps) => JSX.Element;
|
||||
};
|
||||
/**
|
||||
* Function to load more rows once the max number of rows has been reached.
|
||||
*/
|
||||
loadMore: () => void;
|
||||
'data-test-subj'?: string;
|
||||
/**
|
||||
* This is the component that will be rendered in the group selector.
|
||||
* This component will receive the current group and a function to change the group.
|
||||
*/
|
||||
groupSelectorComponent?: JSX.Element;
|
||||
/**
|
||||
* Height override for the data grid.
|
||||
*/
|
||||
height?: number | string;
|
||||
|
||||
/**
|
||||
* This function will be used in the control column to create a rule for a specific finding.
|
||||
*/
|
||||
createRuleFn?: (rowIndex: number) => ((http: HttpSetup) => Promise<RuleResponse>) | undefined;
|
||||
/* Optional props passed to Columns to display Provided Labels as Column name instead of field name */
|
||||
columnHeaders?: Record<string, string>;
|
||||
/**
|
||||
* Specify if distribution bar is shown on data table, used to calculate height of data table in virtualized mode
|
||||
*/
|
||||
hasDistributionBar?: boolean;
|
||||
}
|
||||
|
||||
export const VulnerabilityCloudSecurityDataTable = ({
|
||||
isLoading,
|
||||
defaultColumns,
|
||||
rows,
|
||||
total,
|
||||
flyoutComponent,
|
||||
cloudPostureDataTable,
|
||||
loadMore,
|
||||
title,
|
||||
customCellRenderer,
|
||||
groupSelectorComponent,
|
||||
height,
|
||||
createRuleFn,
|
||||
columnHeaders,
|
||||
hasDistributionBar = true,
|
||||
...rest
|
||||
}: CloudSecurityDataTableProps) => {
|
||||
const {
|
||||
columnsLocalStorageKey,
|
||||
pageSize,
|
||||
onChangeItemsPerPage,
|
||||
setUrlQuery,
|
||||
onSort,
|
||||
onResetFilters,
|
||||
filters,
|
||||
sort,
|
||||
} = cloudPostureDataTable;
|
||||
|
||||
const [columns, setColumns] = useLocalStorage(
|
||||
columnsLocalStorageKey,
|
||||
defaultColumns.map((c) => c.id)
|
||||
);
|
||||
const [persistedSettings, setPersistedSettings] = useLocalStorage<UnifiedDataTableSettings>(
|
||||
`${columnsLocalStorageKey}:settings`,
|
||||
{
|
||||
columns: defaultColumns.reduce((columnSettings, column) => {
|
||||
const columnDefaultSettings = column.width ? { width: column.width } : {};
|
||||
const newColumn = { [column.id]: columnDefaultSettings };
|
||||
return { ...columnSettings, ...newColumn };
|
||||
}, {} as UnifiedDataTableSettings['columns']),
|
||||
}
|
||||
);
|
||||
|
||||
const settings = useMemo(() => {
|
||||
return {
|
||||
columns: Object.keys(persistedSettings?.columns as UnifiedDataTableSettings).reduce(
|
||||
(columnSettings, columnId) => {
|
||||
const newColumn: UnifiedDataTableSettingsColumn = {
|
||||
..._.pick(persistedSettings?.columns?.[columnId], ['width']),
|
||||
display: columnHeaders?.[columnId],
|
||||
};
|
||||
|
||||
return { ...columnSettings, [columnId]: newColumn };
|
||||
},
|
||||
{} as UnifiedDataTableSettings['columns']
|
||||
),
|
||||
};
|
||||
}, [persistedSettings, columnHeaders]);
|
||||
|
||||
const { dataView, dataViewIsRefetching } = useDataViewContext();
|
||||
|
||||
const [expandedDoc, setExpandedDoc] = useState<DataTableRecord | undefined>(undefined);
|
||||
|
||||
const renderDocumentView = (hit: DataTableRecord) =>
|
||||
flyoutComponent(hit, () => setExpandedDoc(undefined));
|
||||
|
||||
// services needed for unified-data-table package
|
||||
const {
|
||||
uiSettings,
|
||||
uiActions,
|
||||
dataViews,
|
||||
data,
|
||||
application,
|
||||
theme,
|
||||
fieldFormats,
|
||||
toastNotifications,
|
||||
storage,
|
||||
} = useKibana().services;
|
||||
|
||||
const styles = useStyles();
|
||||
|
||||
const { capabilities } = application;
|
||||
const { filterManager } = data.query;
|
||||
|
||||
const services = {
|
||||
theme,
|
||||
fieldFormats,
|
||||
uiSettings,
|
||||
toastNotifications,
|
||||
storage,
|
||||
data,
|
||||
};
|
||||
|
||||
const {
|
||||
columns: currentColumns,
|
||||
onSetColumns,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
} = useColumns({
|
||||
capabilities,
|
||||
defaultOrder: uiSettings.get(SORT_DEFAULT_ORDER_SETTING),
|
||||
dataView,
|
||||
dataViews,
|
||||
setAppState: (props) => setColumns(props.columns),
|
||||
columns,
|
||||
sort,
|
||||
});
|
||||
|
||||
/**
|
||||
* This object is used to determine if the table rendering will be virtualized and the virtualization wrapper height.
|
||||
* mode should be passed as a key to the UnifiedDataTable component to force a re-render when the mode changes.
|
||||
*/
|
||||
const computeDataTableRendering = useMemo(() => {
|
||||
// Enable virtualization mode when the table is set to a large page size.
|
||||
const isVirtualizationEnabled = pageSize >= 100;
|
||||
|
||||
const getWrapperHeight = () => {
|
||||
if (height) return height;
|
||||
|
||||
// If virtualization is not needed the table will render unconstrained.
|
||||
if (!isVirtualizationEnabled) return 'auto';
|
||||
|
||||
const baseHeight = 362; // height of Kibana Header + Findings page header and search bar
|
||||
const filterBarHeight = filters?.length > 0 ? 40 : 0;
|
||||
const distributionBarHeight = hasDistributionBar ? 52 : 0;
|
||||
return `calc(100vh - ${baseHeight}px - ${filterBarHeight}px - ${distributionBarHeight}px)`;
|
||||
};
|
||||
|
||||
return {
|
||||
wrapperHeight: getWrapperHeight(),
|
||||
mode: isVirtualizationEnabled ? 'virtualized' : 'standard',
|
||||
};
|
||||
}, [pageSize, height, filters?.length, hasDistributionBar]);
|
||||
|
||||
const onAddFilter: AddFieldFilterHandler | undefined = useMemo(
|
||||
() =>
|
||||
filterManager && dataView
|
||||
? (clickedField, values, operation) => {
|
||||
const newFilters = generateFilters(
|
||||
filterManager,
|
||||
clickedField,
|
||||
values,
|
||||
operation,
|
||||
dataView
|
||||
);
|
||||
filterManager.addFilters(newFilters);
|
||||
setUrlQuery({
|
||||
filters: filterManager.getFilters(),
|
||||
});
|
||||
}
|
||||
: undefined,
|
||||
[dataView, filterManager, setUrlQuery]
|
||||
);
|
||||
|
||||
const onResize = (colSettings: { columnId: string; width: number | undefined }) => {
|
||||
const grid = persistedSettings || {};
|
||||
const newColumns = { ...(grid.columns || {}) };
|
||||
newColumns[colSettings.columnId] = colSettings.width
|
||||
? { width: Math.round(colSettings.width) }
|
||||
: {};
|
||||
const newGrid = { ...grid, columns: newColumns };
|
||||
setPersistedSettings(newGrid);
|
||||
};
|
||||
|
||||
const externalCustomRenderers = useMemo(() => {
|
||||
if (!customCellRenderer) {
|
||||
return undefined;
|
||||
}
|
||||
return customCellRenderer(rows);
|
||||
}, [customCellRenderer, rows]);
|
||||
|
||||
const onResetColumns = () => {
|
||||
setColumns(defaultColumns.map((c) => c.id));
|
||||
};
|
||||
|
||||
if (!isLoading && !rows.length) {
|
||||
return <EmptyState onResetFilters={onResetFilters} />;
|
||||
}
|
||||
|
||||
const externalAdditionalControls = (
|
||||
<AdditionalControls
|
||||
total={total}
|
||||
dataView={dataView}
|
||||
title={title}
|
||||
columns={currentColumns}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
groupSelectorComponent={groupSelectorComponent}
|
||||
onResetColumns={onResetColumns}
|
||||
/>
|
||||
);
|
||||
|
||||
const externalControlColumns: EuiDataGridControlColumn[] | undefined = createRuleFn
|
||||
? [
|
||||
{
|
||||
id: 'select',
|
||||
width: 20,
|
||||
headerCellRender: () => null,
|
||||
rowCellRender: ({ rowIndex }) =>
|
||||
createRuleFn && (
|
||||
<TakeAction isDataGridControlColumn createRuleFn={createRuleFn(rowIndex)} />
|
||||
),
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
|
||||
const rowHeightState = 0;
|
||||
|
||||
const loadingStyle = {
|
||||
opacity: isLoading ? 1 : 0,
|
||||
};
|
||||
|
||||
const loadingState =
|
||||
isLoading || dataViewIsRefetching ? DataLoadingState.loading : DataLoadingState.loaded;
|
||||
|
||||
return (
|
||||
<CellActionsProvider getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}>
|
||||
<div
|
||||
data-test-subj={rest['data-test-subj']}
|
||||
className={styles.gridContainer}
|
||||
style={{
|
||||
height: computeDataTableRendering.wrapperHeight,
|
||||
}}
|
||||
>
|
||||
<EuiProgress size="xs" color="accent" style={loadingStyle} />
|
||||
<UnifiedDataTable
|
||||
key={computeDataTableRendering.mode}
|
||||
className={styles.gridStyle}
|
||||
ariaLabelledBy={title}
|
||||
columns={currentColumns}
|
||||
expandedDoc={expandedDoc}
|
||||
dataView={dataView}
|
||||
loadingState={loadingState}
|
||||
onFilter={onAddFilter as DocViewFilterFn}
|
||||
onResize={onResize}
|
||||
onSetColumns={onSetColumns}
|
||||
onSort={onSort}
|
||||
rows={rows}
|
||||
sampleSizeState={MAX_FINDINGS_TO_LOAD}
|
||||
setExpandedDoc={setExpandedDoc}
|
||||
renderDocumentView={renderDocumentView}
|
||||
sort={sort}
|
||||
rowsPerPageState={pageSize}
|
||||
totalHits={total}
|
||||
services={services}
|
||||
onUpdateRowsPerPage={onChangeItemsPerPage}
|
||||
rowHeightState={rowHeightState}
|
||||
showMultiFields={uiSettings.get(SHOW_MULTIFIELDS)}
|
||||
showTimeCol={false}
|
||||
settings={settings}
|
||||
onFetchMoreRecords={loadMore}
|
||||
externalControlColumns={externalControlColumns}
|
||||
externalCustomRenderers={externalCustomRenderers}
|
||||
externalAdditionalControls={externalAdditionalControls}
|
||||
gridStyleOverride={gridStyle}
|
||||
rowLineHeightOverride="24px"
|
||||
controlColumnIds={controlColumnIds}
|
||||
dataGridDensityState={DataGridDensity.EXPANDED}
|
||||
/>
|
||||
</div>
|
||||
</CellActionsProvider>
|
||||
);
|
||||
};
|
|
@ -25,6 +25,11 @@ import {
|
|||
generateMultipleCspFindings,
|
||||
rulesGetStatesHandler,
|
||||
} from './configurations.handlers.mock';
|
||||
import { useExpandableFlyoutCsp } from '../../common/hooks/use_expandable_flyout_csp';
|
||||
|
||||
jest.mock('../../common/hooks/use_expandable_flyout_csp', () => ({
|
||||
useExpandableFlyoutCsp: jest.fn(),
|
||||
}));
|
||||
|
||||
const server = setupMockServer();
|
||||
|
||||
|
@ -40,6 +45,10 @@ const renderFindingsPage = (dependencies = getMockServerDependencies()) => {
|
|||
describe('<Findings />', () => {
|
||||
startMockServer(server);
|
||||
|
||||
(useExpandableFlyoutCsp as jest.Mock).mockReturnValue({
|
||||
onExpandDocClick: jest.fn(),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
server.use(rulesGetStatesHandler);
|
||||
});
|
||||
|
|
|
@ -9,34 +9,27 @@ import { CDR_MISCONFIGURATIONS_INDEX_PATTERN } from '@kbn/cloud-security-posture
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { FindingsRuleFlyout } from './findings_flyout';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { useMisconfigurationFinding } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_finding';
|
||||
import { TestProvider } from '../../../test/test_provider';
|
||||
import { mockFindingsHit, mockWizFinding } from '../__mocks__/findings';
|
||||
|
||||
const onPaginate = jest.fn();
|
||||
|
||||
const TestComponent = ({ ...overrideProps }) => (
|
||||
const TestComponent = () => (
|
||||
<TestProvider>
|
||||
<FindingsRuleFlyout
|
||||
onClose={jest.fn}
|
||||
flyoutIndex={0}
|
||||
findingsCount={2}
|
||||
onPaginate={onPaginate}
|
||||
finding={mockFindingsHit}
|
||||
{...overrideProps}
|
||||
/>
|
||||
<FindingsRuleFlyout ruleId={'rule_id_test'} resourceId={'resource_id_test'} />
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_finding', () => ({
|
||||
useMisconfigurationFinding: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('<FindingsFlyout/>', () => {
|
||||
describe('Overview Tab', () => {
|
||||
it('details and remediation accordions are open', () => {
|
||||
const { getAllByRole } = render(<TestComponent />);
|
||||
it('should render the flyout with available data', async () => {
|
||||
(useMisconfigurationFinding as jest.Mock).mockReturnValue({
|
||||
data: { result: { hits: [{ _source: mockFindingsHit }] } },
|
||||
});
|
||||
|
||||
getAllByRole('button', { expanded: true, name: 'Details' });
|
||||
getAllByRole('button', { expanded: true, name: 'Remediation' });
|
||||
});
|
||||
|
||||
it('displays text details summary info', () => {
|
||||
const { getAllByText, getByText } = render(<TestComponent />);
|
||||
|
||||
getAllByText(mockFindingsHit.rule.name);
|
||||
|
@ -50,12 +43,18 @@ describe('<FindingsFlyout/>', () => {
|
|||
});
|
||||
|
||||
it('displays missing info callout when data source is not CSP', () => {
|
||||
const { getByText } = render(<TestComponent finding={mockWizFinding} />);
|
||||
(useMisconfigurationFinding as jest.Mock).mockReturnValue({
|
||||
data: { result: { hits: [{ _source: mockWizFinding }] } },
|
||||
});
|
||||
const { getByText } = render(<TestComponent />);
|
||||
getByText('Some fields not provided by Wiz');
|
||||
});
|
||||
|
||||
it('does not display missing info callout when data source is CSP', () => {
|
||||
const { queryByText } = render(<TestComponent finding={mockFindingsHit} />);
|
||||
(useMisconfigurationFinding as jest.Mock).mockReturnValue({
|
||||
data: { result: { hits: [{ _source: mockFindingsHit }] } },
|
||||
});
|
||||
const { queryByText } = render(<TestComponent />);
|
||||
const missingInfoCallout = queryByText('Some fields not provided by Wiz');
|
||||
expect(missingInfoCallout).toBeNull();
|
||||
});
|
||||
|
@ -75,14 +74,20 @@ describe('<FindingsFlyout/>', () => {
|
|||
});
|
||||
|
||||
it('displays missing info callout when data source is not CSP', async () => {
|
||||
const { getByText } = render(<TestComponent finding={mockWizFinding} />);
|
||||
(useMisconfigurationFinding as jest.Mock).mockReturnValue({
|
||||
data: { result: { hits: [{ _source: mockWizFinding }] } },
|
||||
});
|
||||
const { getByText } = render(<TestComponent />);
|
||||
await userEvent.click(screen.getByTestId('findings_flyout_tab_rule'));
|
||||
|
||||
getByText('Some fields not provided by Wiz');
|
||||
});
|
||||
|
||||
it('does not display missing info callout when data source is CSP', async () => {
|
||||
const { queryByText } = render(<TestComponent finding={mockFindingsHit} />);
|
||||
(useMisconfigurationFinding as jest.Mock).mockReturnValue({
|
||||
data: { result: { hits: [{ _source: mockFindingsHit }] } },
|
||||
});
|
||||
const { queryByText } = render(<TestComponent />);
|
||||
await userEvent.click(screen.getByTestId('findings_flyout_tab_rule'));
|
||||
|
||||
const missingInfoCallout = queryByText('Some fields not provided by Wiz');
|
||||
|
@ -92,6 +97,9 @@ describe('<FindingsFlyout/>', () => {
|
|||
|
||||
describe('Table Tab', () => {
|
||||
it('displays resource name and id', async () => {
|
||||
(useMisconfigurationFinding as jest.Mock).mockReturnValue({
|
||||
data: { result: { hits: [{ _source: mockFindingsHit }] } },
|
||||
});
|
||||
const { getAllByText } = render(<TestComponent />);
|
||||
await userEvent.click(screen.getByTestId('findings_flyout_tab_table'));
|
||||
|
||||
|
@ -100,7 +108,10 @@ describe('<FindingsFlyout/>', () => {
|
|||
});
|
||||
|
||||
it('does not display missing info callout for 3Ps', async () => {
|
||||
const { queryByText } = render(<TestComponent finding={mockWizFinding} />);
|
||||
(useMisconfigurationFinding as jest.Mock).mockReturnValue({
|
||||
data: { result: { hits: [{ _source: mockWizFinding }] } },
|
||||
});
|
||||
const { queryByText } = render(<TestComponent />);
|
||||
await userEvent.click(screen.getByTestId('findings_flyout_tab_table'));
|
||||
|
||||
const missingInfoCallout = queryByText('Some fields not provided by Wiz');
|
||||
|
@ -110,27 +121,14 @@ describe('<FindingsFlyout/>', () => {
|
|||
|
||||
describe('JSON Tab', () => {
|
||||
it('does not display missing info callout for 3Ps', async () => {
|
||||
const { queryByText } = render(<TestComponent finding={mockWizFinding} />);
|
||||
(useMisconfigurationFinding as jest.Mock).mockReturnValue({
|
||||
data: { result: { hits: [{ _source: mockWizFinding }] } },
|
||||
});
|
||||
const { queryByText } = render(<TestComponent />);
|
||||
await userEvent.click(screen.getByTestId('findings_flyout_tab_json'));
|
||||
|
||||
const missingInfoCallout = queryByText('Some fields not provided by Wiz');
|
||||
expect(missingInfoCallout).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow pagination with next', async () => {
|
||||
const { getByTestId } = render(<TestComponent />);
|
||||
|
||||
await userEvent.click(getByTestId('pagination-button-next'));
|
||||
|
||||
expect(onPaginate).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should allow pagination with previous', async () => {
|
||||
const { getByTestId } = render(<TestComponent flyoutIndex={1} />);
|
||||
|
||||
await userEvent.click(getByTestId('pagination-button-previous'));
|
||||
|
||||
expect(onPaginate).toHaveBeenCalledWith(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
useEuiTheme,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiTextColor,
|
||||
EuiFlyout,
|
||||
EuiFlyoutHeader,
|
||||
EuiTitle,
|
||||
EuiFlyoutBody,
|
||||
|
@ -21,12 +21,13 @@ import {
|
|||
EuiCodeBlock,
|
||||
EuiMarkdownFormat,
|
||||
EuiIcon,
|
||||
EuiPagination,
|
||||
EuiFlyoutFooter,
|
||||
EuiToolTip,
|
||||
EuiDescriptionListProps,
|
||||
EuiCallOut,
|
||||
EuiLink,
|
||||
EuiPanel,
|
||||
EuiIconProps,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { assertNever } from '@kbn/std';
|
||||
|
@ -34,7 +35,11 @@ import { i18n } from '@kbn/i18n';
|
|||
import type { HttpSetup } from '@kbn/core/public';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { css } from '@emotion/react';
|
||||
import { CspEvaluationBadge, benchmarksNavigation } from '@kbn/cloud-security-posture';
|
||||
import {
|
||||
CspEvaluationBadge,
|
||||
benchmarksNavigation,
|
||||
createMisconfigurationFindingsQuery,
|
||||
} from '@kbn/cloud-security-posture';
|
||||
import type { CspFinding, BenchmarkId } from '@kbn/cloud-security-posture-common';
|
||||
import { BenchmarkName, CSP_MISCONFIGURATIONS_DATASET } from '@kbn/cloud-security-posture-common';
|
||||
import { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/csp_vulnerability_finding';
|
||||
|
@ -43,8 +48,12 @@ import { getVendorName } from '@kbn/cloud-security-posture/src/utils/get_vendor_
|
|||
import { truthy } from '@kbn/cloud-security-posture/src/utils/helpers';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import type { CspClientPluginStartDeps } from '@kbn/cloud-security-posture';
|
||||
import { createDetectionRuleFromBenchmarkRule } from '@kbn/cloud-security-posture/src/utils/create_detection_rule_from_benchmark'; //
|
||||
import type {
|
||||
CspClientPluginStartDeps,
|
||||
FindingMisconfigurationFlyoutProps,
|
||||
} from '@kbn/cloud-security-posture';
|
||||
import { useMisconfigurationFinding } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_finding';
|
||||
import { createDetectionRuleFromBenchmarkRule } from '@kbn/cloud-security-posture/src/utils/create_detection_rule_from_benchmark';
|
||||
import cisLogoIcon from '../../../assets/icons/cis_logo.svg';
|
||||
import { TakeAction } from '../../../components/take_action';
|
||||
import { TableTab } from './table_tab';
|
||||
|
@ -85,22 +94,10 @@ const tabs = [
|
|||
},
|
||||
] as const;
|
||||
|
||||
const PAGINATION_LABEL = i18n.translate('xpack.csp.findings.findingsFlyout.paginationLabel', {
|
||||
defaultMessage: 'Finding navigation',
|
||||
});
|
||||
|
||||
type FindingsTab = (typeof tabs)[number];
|
||||
|
||||
export const EMPTY_VALUE = '-';
|
||||
|
||||
interface FindingFlyoutProps {
|
||||
onClose(): void;
|
||||
finding: CspFinding;
|
||||
flyoutIndex?: number;
|
||||
findingsCount?: number;
|
||||
onPaginate?: (pageIndex: number) => void;
|
||||
}
|
||||
|
||||
export const CodeBlock: React.FC<PropsOf<typeof EuiCodeBlock>> = (props) => (
|
||||
<EuiCodeBlock isCopyable paddingSize="s" overflowHeight={300} {...props} />
|
||||
);
|
||||
|
@ -112,20 +109,22 @@ export const CspFlyoutMarkdown: React.FC<PropsOf<typeof EuiMarkdownFormat>> = (p
|
|||
export const BenchmarkIcons = ({
|
||||
benchmarkId,
|
||||
benchmarkName,
|
||||
size = 'xl',
|
||||
}: {
|
||||
benchmarkId: BenchmarkId;
|
||||
benchmarkName: BenchmarkName;
|
||||
size?: EuiIconProps['size'];
|
||||
}) => (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
{benchmarkId.startsWith('cis') && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content="Center for Internet Security">
|
||||
<EuiIcon type={cisLogoIcon} size="xl" />
|
||||
<EuiIcon type={cisLogoIcon} size={size} />
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<CISBenchmarkIcon type={benchmarkId} name={benchmarkName} />
|
||||
<CISBenchmarkIcon type={benchmarkId} name={benchmarkName} size={size} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
@ -232,89 +231,94 @@ export const MissingFieldsCallout = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const FindingsRuleFlyout = ({
|
||||
onClose,
|
||||
finding,
|
||||
flyoutIndex,
|
||||
findingsCount,
|
||||
onPaginate,
|
||||
}: FindingFlyoutProps) => {
|
||||
export const FindingsRuleFlyout = ({ ruleId, resourceId }: FindingMisconfigurationFlyoutProps) => {
|
||||
const { data } = useMisconfigurationFinding({
|
||||
query: createMisconfigurationFindingsQuery(resourceId, ruleId),
|
||||
enabled: true,
|
||||
pageSize: 1,
|
||||
});
|
||||
const finding = data?.result.hits[0]._source;
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const [tab, setTab] = useState<FindingsTab>(tabs[0]);
|
||||
|
||||
if (!finding) return null;
|
||||
|
||||
const createMisconfigurationRuleFn = async (http: HttpSetup) =>
|
||||
await createDetectionRuleFromBenchmarkRule(http, finding.rule);
|
||||
createDetectionRuleFromBenchmarkRule(http, finding?.rule);
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onClose} data-test-subj={FINDINGS_FLYOUT}>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<CspEvaluationBadge type={finding.result?.evaluation} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow style={{ minWidth: 0 }}>
|
||||
<EuiTitle size="m" className="eui-textTruncate">
|
||||
<EuiTextColor color="primary" title={finding.rule?.name}>
|
||||
{finding.rule?.name}
|
||||
</EuiTextColor>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<div
|
||||
css={css`
|
||||
line-height: 20px;
|
||||
margin-top: ${euiTheme.size.m};
|
||||
`}
|
||||
>
|
||||
<CspInlineDescriptionList
|
||||
testId={FINDINGS_MISCONFIGS_FLYOUT_DESCRIPTION_LIST}
|
||||
listItems={getFlyoutDescriptionList(finding)}
|
||||
/>
|
||||
</div>
|
||||
<EuiSpacer />
|
||||
<EuiTabs>
|
||||
{tabs.map((v) => (
|
||||
<EuiTab
|
||||
key={v.id}
|
||||
isSelected={tab.id === v.id}
|
||||
onClick={() => setTab(v)}
|
||||
data-test-subj={`findings_flyout_tab_${v.id}`}
|
||||
>
|
||||
{v.title}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody key={tab.id}>
|
||||
{!isNativeCspFinding(finding) && ['overview', 'rule'].includes(tab.id) && (
|
||||
<div style={{ marginBottom: euiTheme.size.base }}>
|
||||
<MissingFieldsCallout finding={finding} />
|
||||
</div>
|
||||
<>
|
||||
<EuiFlexGroup gutterSize={'none'} direction={'column'} data-test-subj={FINDINGS_FLYOUT}>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiPanel hasShadow={false}>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<CspEvaluationBadge type={finding?.result?.evaluation} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow style={{ minWidth: 0 }}>
|
||||
<EuiTitle size="m" className="eui-textTruncate">
|
||||
<EuiTextColor color="primary" title={finding?.rule?.name}>
|
||||
{finding?.rule?.name}
|
||||
</EuiTextColor>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{finding && (
|
||||
<div
|
||||
css={css`
|
||||
line-height: 20px;
|
||||
margin-top: ${euiTheme.size.m};
|
||||
`}
|
||||
>
|
||||
<CspInlineDescriptionList
|
||||
testId={FINDINGS_MISCONFIGS_FLYOUT_DESCRIPTION_LIST}
|
||||
listItems={getFlyoutDescriptionList(finding)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EuiSpacer />
|
||||
<EuiTabs>
|
||||
{tabs.map((v) => (
|
||||
<EuiTab
|
||||
key={v.id}
|
||||
isSelected={tab.id === v.id}
|
||||
onClick={() => setTab(v)}
|
||||
data-test-subj={`findings_flyout_tab_${v.id}`}
|
||||
>
|
||||
{v.title}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
</EuiPanel>
|
||||
</EuiFlyoutHeader>
|
||||
{finding && (
|
||||
<EuiPanel hasShadow={false}>
|
||||
<EuiFlyoutBody key={tab.id}>
|
||||
{!isNativeCspFinding(finding) && ['overview', 'rule'].includes(tab.id) && (
|
||||
<div style={{ marginBottom: euiTheme.size.base }}>
|
||||
<MissingFieldsCallout finding={finding} />
|
||||
</div>
|
||||
)}
|
||||
<FindingsTab tab={tab} finding={finding} />
|
||||
</EuiFlyoutBody>
|
||||
</EuiPanel>
|
||||
)}
|
||||
<FindingsTab tab={tab} finding={finding} />
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
alignItems="center"
|
||||
justifyContent={onPaginate ? 'spaceBetween' : 'flexEnd'}
|
||||
>
|
||||
{onPaginate && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPagination
|
||||
aria-label={PAGINATION_LABEL}
|
||||
pageCount={findingsCount}
|
||||
activePage={flyoutIndex}
|
||||
onPageClick={onPaginate}
|
||||
compressed
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<TakeAction createRuleFn={createMisconfigurationRuleFn} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiPanel color="transparent">
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<TakeAction createRuleFn={createMisconfigurationRuleFn} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default FindingsRuleFlyout;
|
||||
|
|
|
@ -27,7 +27,6 @@ import { CloudSecurityDataTable } from '../../../components/cloud_security_data_
|
|||
import { getDefaultQuery, defaultColumns } from './constants';
|
||||
import { useLatestFindingsTable } from './use_latest_findings_table';
|
||||
import { TimestampTableCell } from '../../../components/timestamp_table_cell';
|
||||
import { FindingsRuleFlyout } from '../findings_flyout/findings_flyout';
|
||||
import { findingsTableFieldLabels } from './findings_table_field_labels';
|
||||
|
||||
interface LatestFindingsTableProps {
|
||||
|
@ -49,13 +48,11 @@ const getCspFinding = (source: Record<string, any> | undefined): CspFinding | un
|
|||
};
|
||||
|
||||
/**
|
||||
* Flyout component for the latest findings table
|
||||
* Flyout component for the latest findings table, renders empty component as now we use Use Expandable Flyout API hook to handle all the rendering
|
||||
*/
|
||||
const flyoutComponent = (row: DataTableRecord, onCloseFlyout: () => void): JSX.Element => {
|
||||
const finding = row.raw._source;
|
||||
if (!finding || !isCspFinding(finding)) return <></>;
|
||||
const onOpenFlyoutCallback = (): JSX.Element => {
|
||||
uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, OPEN_FINDINGS_FLYOUT);
|
||||
return <FindingsRuleFlyout finding={finding} onClose={onCloseFlyout} />;
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const title = i18n.translate('xpack.csp.findings.latestFindings.tableRowTypeLabel', {
|
||||
|
@ -141,7 +138,7 @@ export const LatestFindingsTable = ({
|
|||
defaultColumns={defaultColumns}
|
||||
rows={rows}
|
||||
total={total}
|
||||
flyoutComponent={flyoutComponent}
|
||||
onOpenFlyoutCallback={onOpenFlyoutCallback}
|
||||
cloudPostureDataTable={cloudPostureDataTable}
|
||||
loadMore={fetchNextPage}
|
||||
title={title}
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
getNormalizedSeverity,
|
||||
} from '@kbn/cloud-security-posture';
|
||||
import { getVendorName } from '@kbn/cloud-security-posture/src/utils/get_vendor_name';
|
||||
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';
|
||||
|
@ -26,6 +25,7 @@ import { VulnerabilityFindingFlyout } from './vulnerabilities_finding_flyout/vul
|
|||
import { ErrorCallout } from '../configurations/layout/error_callout';
|
||||
import { createDetectionRuleFromVulnerabilityFinding } from './utils/create_detection_rule_from_vulnerability';
|
||||
import { vulnerabilitiesTableFieldLabels } from './vulnerabilities_table_field_labels';
|
||||
import { VulnerabilityCloudSecurityDataTable } from '../../components/cloud_security_data_table/vulnerability_security_data_table';
|
||||
|
||||
interface LatestVulnerabilitiesTableProps {
|
||||
groupSelectorComponent?: JSX.Element;
|
||||
|
@ -171,7 +171,7 @@ export const LatestVulnerabilitiesTable = ({
|
|||
<ErrorCallout error={error} />
|
||||
</>
|
||||
) : (
|
||||
<CloudSecurityDataTable
|
||||
<VulnerabilityCloudSecurityDataTable
|
||||
data-test-subj={LATEST_VULNERABILITIES_TABLE}
|
||||
isLoading={isFetching}
|
||||
defaultColumns={defaultColumns}
|
||||
|
|
|
@ -9,7 +9,10 @@ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kb
|
|||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
|
||||
import type { CspClientPluginStartDeps } from '@kbn/cloud-security-posture';
|
||||
import type {
|
||||
CspClientPluginStartDeps,
|
||||
FindingMisconfigurationFlyoutProps,
|
||||
} from '@kbn/cloud-security-posture';
|
||||
import { uiMetricService } from '@kbn/cloud-security-posture-common/utils/ui_metrics';
|
||||
import { CspLoadingState } from './components/csp_loading_state';
|
||||
import type { CspRouterProps } from './application/csp_router';
|
||||
|
@ -31,6 +34,10 @@ const LazyCspCustomAssets = lazy(
|
|||
() => import('./components/fleet_extensions/custom_assets_extension')
|
||||
);
|
||||
|
||||
const LazyCspFindingsMisconfigurationFlyout = lazy(
|
||||
() => import('./pages/configurations/findings_flyout/findings_flyout')
|
||||
);
|
||||
|
||||
const CspRouterLazy = lazy(() => import('./application/csp_router'));
|
||||
const CspRouter = (props: CspRouterProps) => (
|
||||
<Suspense fallback={<CspLoadingState />}>
|
||||
|
@ -101,6 +108,12 @@ export class CspPlugin
|
|||
|
||||
return {
|
||||
getCloudSecurityPostureRouter: () => App,
|
||||
getCloudSecurityPostureMisconfigurationFlyout: ({
|
||||
ruleId,
|
||||
resourceId,
|
||||
}: FindingMisconfigurationFlyoutProps) => (
|
||||
<LazyCspFindingsMisconfigurationFlyout ruleId={ruleId} resourceId={resourceId} />
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -12,9 +12,18 @@ import type { DataPublicPluginSetup } from '@kbn/data-plugin/public';
|
|||
import { CoreStart } from '@kbn/core/public';
|
||||
import type { FleetSetup } from '@kbn/fleet-plugin/public';
|
||||
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import { ExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { FindingMisconfigurationFlyoutProps } from '@kbn/cloud-security-posture';
|
||||
import type { CspRouterProps } from './application/csp_router';
|
||||
import type { CloudSecurityPosturePageId } from './common/navigation/types';
|
||||
|
||||
export interface UseOnCloseParams {
|
||||
/**
|
||||
* Function to call when the event is dispatched
|
||||
*/
|
||||
callback: (id: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The cloud security posture's public plugin setup interface.
|
||||
*/
|
||||
|
@ -27,6 +36,10 @@ export interface CspClientPluginSetup {}
|
|||
export interface CspClientPluginStart {
|
||||
/** Gets the cloud security posture router component for embedding in the security solution. */
|
||||
getCloudSecurityPostureRouter(): ComponentType<CspRouterProps>;
|
||||
getCloudSecurityPostureMisconfigurationFlyout: ({
|
||||
ruleId,
|
||||
resourceId,
|
||||
}: FindingMisconfigurationFlyoutProps) => React.JSX.Element;
|
||||
}
|
||||
|
||||
export interface CspClientPluginSetupDeps {
|
||||
|
@ -50,6 +63,8 @@ export interface CspSecuritySolutionContext {
|
|||
pageName: CloudSecurityPosturePageId;
|
||||
state?: Record<string, string | undefined>;
|
||||
}>;
|
||||
useExpandableFlyoutApi?: () => ExpandableFlyoutApi;
|
||||
useOnExpandableFlyoutClose?: ({ callback }: UseOnCloseParams) => void;
|
||||
}
|
||||
|
||||
export type CloudSecurityPostureStartServices = Pick<
|
||||
|
|
|
@ -66,6 +66,7 @@
|
|||
"@kbn/cloud-security-posture",
|
||||
"@kbn/analytics",
|
||||
"@kbn/management-settings-ids",
|
||||
"@kbn/expandable-flyout",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -10,12 +10,14 @@ import { CLOUD_SECURITY_POSTURE_BASE_PATH } from '@kbn/cloud-security-posture-co
|
|||
import type { CloudSecurityPosturePageId } from '@kbn/cloud-security-posture-plugin/public';
|
||||
import { type CspSecuritySolutionContext } from '@kbn/cloud-security-posture-plugin/public';
|
||||
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import type { SecurityPageName, SecuritySubPluginRoutes } from '../app/types';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { SecuritySolutionPageWrapper } from '../common/components/page_wrapper';
|
||||
import { SpyRoute } from '../common/utils/route/spy_routes';
|
||||
import { FiltersGlobal } from '../common/components/filters_global';
|
||||
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
|
||||
import { useOnExpandableFlyoutClose } from '../flyout/shared/hooks/use_on_expandable_flyout_close';
|
||||
|
||||
// This exists only for the type signature cast
|
||||
const CloudPostureSpyRoute = ({ pageName, ...rest }: { pageName?: CloudSecurityPosturePageId }) => (
|
||||
|
@ -25,6 +27,8 @@ const CloudPostureSpyRoute = ({ pageName, ...rest }: { pageName?: CloudSecurityP
|
|||
const cspSecuritySolutionContext: CspSecuritySolutionContext = {
|
||||
getFiltersGlobalComponent: () => FiltersGlobal,
|
||||
getSpyRouteComponent: () => CloudPostureSpyRoute,
|
||||
useExpandableFlyoutApi,
|
||||
useOnExpandableFlyoutClose,
|
||||
};
|
||||
|
||||
const CloudSecurityPosture = () => {
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { FindingsMisconfigurationPanelExpandableFlyoutProps } from '@kbn/cloud-security-posture';
|
||||
|
||||
export const MisconfigurationFindingsPanelKey: FindingsMisconfigurationPanelExpandableFlyoutProps['key'] =
|
||||
'findings-misconfiguration-panel';
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { FindingMisconfigurationFlyoutProps } from '@kbn/cloud-security-posture';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
|
||||
export const FindingsMisconfigurationPanel = ({
|
||||
ruleId,
|
||||
resourceId,
|
||||
}: FindingMisconfigurationFlyoutProps) => {
|
||||
const { cloudSecurityPosture } = useKibana().services;
|
||||
return cloudSecurityPosture.getCloudSecurityPostureMisconfigurationFlyout({ ruleId, resourceId });
|
||||
};
|
|
@ -8,6 +8,7 @@
|
|||
import React, { memo, useCallback } from 'react';
|
||||
import { ExpandableFlyout, type ExpandableFlyoutProps } from '@kbn/expandable-flyout';
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import type { FindingsMisconfigurationPanelExpandableFlyoutProps } from '@kbn/cloud-security-posture';
|
||||
import type { UniversalEntityPanelExpandableFlyoutProps } from './entity_details/universal_right';
|
||||
import { UniversalEntityPanel } from './entity_details/universal_right';
|
||||
import { SessionViewPanelProvider } from './document_details/session_view/context';
|
||||
|
@ -58,6 +59,8 @@ import type { ServicePanelExpandableFlyoutProps } from './entity_details/service
|
|||
import { ServicePanel } from './entity_details/service_right';
|
||||
import type { ServiceDetailsExpandableFlyoutProps } from './entity_details/service_details_left';
|
||||
import { ServiceDetailsPanel, ServiceDetailsPanelKey } from './entity_details/service_details_left';
|
||||
import { FindingsMisconfigurationPanel } from './csp_details/findings_flyout/helper';
|
||||
import { MisconfigurationFindingsPanelKey } from './csp_details/findings_flyout/constants';
|
||||
|
||||
/**
|
||||
* List of all panels that will be used within the document details expandable flyout.
|
||||
|
@ -187,6 +190,14 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels']
|
|||
<UniversalEntityPanel {...(props as UniversalEntityPanelExpandableFlyoutProps).params} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: MisconfigurationFindingsPanelKey,
|
||||
component: (props) => (
|
||||
<FindingsMisconfigurationPanel
|
||||
{...(props as FindingsMisconfigurationPanelExpandableFlyoutProps).params}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const SECURITY_SOLUTION_ON_CLOSE_EVENT = `expandable-flyout-on-close-${Flyouts.securitySolution}`;
|
||||
|
|
|
@ -24,6 +24,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
resource: { id: chance.guid(), name: `kubelet`, sub_type: 'lower case sub type' },
|
||||
result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' },
|
||||
rule: {
|
||||
id: chance.guid(),
|
||||
tags: ['CIS', 'CIS K8S'],
|
||||
rationale: 'rationale steps for rule 1.1',
|
||||
references: '1. https://elastic.co/rules/1.1',
|
||||
|
@ -49,6 +50,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
resource: { id: chance.guid(), name: `Pod`, sub_type: 'Upper case sub type' },
|
||||
result: { evaluation: chance.integer() % 2 === 0 ? 'passed' : 'failed' },
|
||||
rule: {
|
||||
id: chance.guid(),
|
||||
tags: ['CIS', 'CIS K8S'],
|
||||
rationale: 'rationale steps',
|
||||
references: '1. https://elastic.co',
|
||||
|
@ -74,6 +76,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
resource: { id: chance.guid(), name: `process`, sub_type: 'another lower case type' },
|
||||
result: { evaluation: 'passed' },
|
||||
rule: {
|
||||
id: chance.guid(),
|
||||
tags: ['CIS', 'CIS K8S'],
|
||||
rationale: 'rationale steps',
|
||||
references: '1. https://elastic.co',
|
||||
|
@ -99,6 +102,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
resource: { id: chance.guid(), name: `process`, sub_type: 'Upper case type again' },
|
||||
result: { evaluation: 'failed' },
|
||||
rule: {
|
||||
id: chance.guid(),
|
||||
tags: ['CIS', 'CIS K8S'],
|
||||
rationale: 'rationale steps',
|
||||
references: '1. https://elastic.co',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue