mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Cloud Security ] 12165 update UI handling of multiple CVEs and package fields (#216411)
## Summary This PR updates the rendering of multi value fields - vulnerability.id, package.name, package.version and package.fixed_version in the vulnerabilities data-grid page and alerts insights vulnerabilities tab data grid. It also updates the rendering of package.* fields in the vulnerabilities flyout and both flyout and data grids are re using the same kbn package component to display it. ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Make CVSS column to be displayed first in the data grid. - [x] if there is a single CVE display its value across the data grid. - [x] data-grid if there is more than a single CVE show it as <first_cve> <+x more> badge indicating the number of CVES left. Clicking on the badge should open a Popver where all CVEs are displayed as badges - clicking on the value will add it to the search bar filters, each batch should have a copy icon as well. - [x] insights tab data-grid should have similar logic to display multi value fields but without adding it to the filters logic since there are no filters in that page. - [x] logic of displaying multiple CVEs should be applied to package.name, package.version and package.fixed_version fields in both data grids. - [x] arrays in package-related vulnerability fields are rendered correctly in the flyout header and footer. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Screen recording https://github.com/user-attachments/assets/208f8445-83c1-4e8f-a490-85ec48830fae --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
9cc220ac52
commit
854bfc4964
21 changed files with 817 additions and 129 deletions
|
@ -21,3 +21,6 @@ export { getVulnerabilityStats, hasVulnerabilitiesData } from './src/utils/vulne
|
|||
export { CVSScoreBadge, SeverityStatusBadge } from './src/components/vulnerability_badges';
|
||||
export { getNormalizedSeverity } from './src/utils/get_normalized_severity';
|
||||
export { createMisconfigurationFindingsQuery } from './src/utils/findings_query_builders';
|
||||
export { ActionableBadge, type MultiValueCellAction } from './src/components/actionable_badge';
|
||||
export { MultiValueCellPopover } from './src/components/multi_value_cell_popover';
|
||||
export { findReferenceLink } from './src/utils/find_reference_link.util';
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState, useRef } from 'react';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiText,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
useEuiTheme,
|
||||
EuiCopy,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
const copyItem = i18n.translate('securitySolutionPackages.csp.actionableBadge.copy', {
|
||||
defaultMessage: 'Copy',
|
||||
});
|
||||
|
||||
export interface MultiValueCellAction {
|
||||
iconType: string;
|
||||
onClick?: () => void;
|
||||
ariaLabel: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface ActionableBadgeProps {
|
||||
item: string;
|
||||
index: number;
|
||||
actions?: MultiValueCellAction[];
|
||||
}
|
||||
|
||||
export const ActionableBadge = ({ item, index, actions = [] }: ActionableBadgeProps) => {
|
||||
const [showActions, setShowActions] = useState(false);
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const buttonRef = useRef<HTMLDivElement | null>(null);
|
||||
const iconsContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [buttonPosition, setButtonPosition] = useState({ bottom: 0, left: 0 });
|
||||
|
||||
const updatePosition = () => {
|
||||
if (buttonRef.current && iconsContainerRef.current) {
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
const myElement = iconsContainerRef.current.getBoundingClientRect();
|
||||
|
||||
setButtonPosition({
|
||||
bottom: window.innerHeight - rect.top, // offset from the top of the badge
|
||||
left: rect.right - myElement.width, // align with right edge
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const buttonIconCss = css`
|
||||
color: ${euiTheme.colors.plainLight};
|
||||
inline-size: ${euiTheme.base}px;
|
||||
block-size: ${euiTheme.base}px;
|
||||
svg {
|
||||
width: ${euiTheme.size.m};
|
||||
height: ${euiTheme.size.m};
|
||||
`;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (showActions) {
|
||||
updatePosition();
|
||||
}
|
||||
}, [showActions]);
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setShowActions(false);
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setShowActions(true);
|
||||
};
|
||||
|
||||
const actionButtons = createPortal(
|
||||
<EuiFlexGroup
|
||||
ref={iconsContainerRef}
|
||||
gutterSize="xs"
|
||||
alignItems="flexEnd"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
responsive={false}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
css={css`
|
||||
position: fixed;
|
||||
z-index: ${euiTheme.levels.modal};
|
||||
background-color: ${euiTheme.colors.primary};
|
||||
border-radius: ${euiTheme.size.xs};
|
||||
padding: ${euiTheme.size.xxs};
|
||||
`}
|
||||
style={{
|
||||
bottom: buttonPosition.bottom,
|
||||
left: buttonPosition.left,
|
||||
}}
|
||||
>
|
||||
{actions.map((action, actionIndex) => (
|
||||
<EuiFlexItem grow={false} key={`${action.iconType}-${actionIndex}`}>
|
||||
<EuiButtonIcon
|
||||
css={buttonIconCss}
|
||||
onClick={action.onClick}
|
||||
iconType={action.iconType}
|
||||
aria-label={action.ariaLabel}
|
||||
title={action.title}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCopy textToCopy={item}>
|
||||
{(copy) => (
|
||||
<EuiButtonIcon
|
||||
onClick={(event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.stopPropagation();
|
||||
copy();
|
||||
}}
|
||||
iconType="copy"
|
||||
css={buttonIconCss}
|
||||
aria-label={copyItem}
|
||||
title={copyItem}
|
||||
/>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>,
|
||||
document.body
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={buttonRef}
|
||||
css={css`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
`}
|
||||
>
|
||||
{showActions && actionButtons}
|
||||
<EuiBadge
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
color="hollow"
|
||||
key={`${item}-${index}`}
|
||||
data-test-subj={`multi-value-copy-badge-${index}`}
|
||||
>
|
||||
<EuiText
|
||||
css={css`
|
||||
text-overflow: ellipsis;
|
||||
`}
|
||||
size="m"
|
||||
>
|
||||
{item}
|
||||
</EuiText>
|
||||
</EuiBadge>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 { act } from 'react-dom/test-utils';
|
||||
import { render, fireEvent, screen } from '@testing-library/react';
|
||||
import { MultiValueCellPopover } from './multi_value_cell_popover';
|
||||
import { EuiBadge, EuiText } from '@elastic/eui';
|
||||
import { MULTI_VALUE_CELL_FIRST_ITEM_VALUE, MULTI_VALUE_CELL_MORE_BUTTON } from '../constants';
|
||||
|
||||
const RENDER_ITEM_TEST_ID = 'item-renderer-test-id';
|
||||
|
||||
// Mock EUI theme hook
|
||||
jest.mock('@elastic/eui', () => ({
|
||||
...jest.requireActual('@elastic/eui'),
|
||||
useEuiTheme: () => ({ euiTheme: { size: { s: '8px' } } }),
|
||||
}));
|
||||
|
||||
describe('MultiValueCellPopover', () => {
|
||||
const mockObject = { id: '1' };
|
||||
const defaultRenderItem = (item: string) => (
|
||||
<EuiBadge data-test-subj={RENDER_ITEM_TEST_ID}>{item}</EuiBadge>
|
||||
);
|
||||
|
||||
it('renders single item without badge', () => {
|
||||
render(
|
||||
<MultiValueCellPopover
|
||||
items={['item1']}
|
||||
field="test"
|
||||
object={mockObject}
|
||||
renderItem={defaultRenderItem}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId(MULTI_VALUE_CELL_FIRST_ITEM_VALUE)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(MULTI_VALUE_CELL_FIRST_ITEM_VALUE)).toHaveTextContent('item1');
|
||||
expect(screen.queryByTestId(MULTI_VALUE_CELL_MORE_BUTTON)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders multiple items with badge', () => {
|
||||
render(
|
||||
<MultiValueCellPopover
|
||||
items={['item1', 'item2', 'item3']}
|
||||
field="test"
|
||||
object={mockObject}
|
||||
renderItem={defaultRenderItem}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId(MULTI_VALUE_CELL_FIRST_ITEM_VALUE)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(MULTI_VALUE_CELL_FIRST_ITEM_VALUE)).toHaveTextContent('item1');
|
||||
expect(screen.getByTestId(MULTI_VALUE_CELL_MORE_BUTTON)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(MULTI_VALUE_CELL_MORE_BUTTON)).toHaveTextContent('+ 2');
|
||||
});
|
||||
|
||||
it('opens popover on badge click', async () => {
|
||||
render(
|
||||
<MultiValueCellPopover
|
||||
items={['item1', 'item2']}
|
||||
field="test"
|
||||
object={mockObject}
|
||||
renderItem={defaultRenderItem}
|
||||
/>
|
||||
);
|
||||
|
||||
const badge = screen.getByText('+ 1');
|
||||
act(() => {
|
||||
fireEvent.click(badge);
|
||||
});
|
||||
|
||||
expect(screen.getAllByTestId(RENDER_ITEM_TEST_ID)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('uses custom firstItemRenderer when provided', () => {
|
||||
const customRenderTestId = 'custom-renderer-test-id';
|
||||
|
||||
const customRenderer = (item: string) => (
|
||||
<EuiText data-test-subj={customRenderTestId}>{`Custom ${item}`}</EuiText>
|
||||
);
|
||||
|
||||
render(
|
||||
<MultiValueCellPopover
|
||||
items={['item1']}
|
||||
field="test"
|
||||
object={mockObject}
|
||||
renderItem={defaultRenderItem}
|
||||
firstItemRenderer={customRenderer}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId(customRenderTestId)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(customRenderTestId)).toHaveTextContent('Custom item1');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import {
|
||||
EuiPopover,
|
||||
EuiBadge,
|
||||
useEuiTheme,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { MULTI_VALUE_CELL_FIRST_ITEM_VALUE, MULTI_VALUE_CELL_MORE_BUTTON } from '../constants';
|
||||
|
||||
const getShowMoreAriaLabel = (field: string, number: number) =>
|
||||
i18n.translate('securitySolutionPackages.csp.multiValueField.showMore', {
|
||||
defaultMessage: 'Field {field} has {number, plural, one {# item} other {#items}}',
|
||||
values: { field, number },
|
||||
});
|
||||
|
||||
interface MultiValueCellPopoverProps<T, K = string> {
|
||||
items: K[];
|
||||
field: string;
|
||||
object: T;
|
||||
renderItem: (item: K, i: number, field: string, object: T) => React.ReactNode;
|
||||
firstItemRenderer?: (item: K) => React.ReactNode;
|
||||
}
|
||||
|
||||
const MultiValueCellPopoverComponent = <T, K = string>({
|
||||
items,
|
||||
field,
|
||||
object,
|
||||
renderItem,
|
||||
firstItemRenderer,
|
||||
}: MultiValueCellPopoverProps<T, K>) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const onButtonClick = () => setIsPopoverOpen(!isPopoverOpen);
|
||||
const closePopover = () => setIsPopoverOpen(false);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup responsive={true} gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{firstItemRenderer ? (
|
||||
firstItemRenderer(items[0])
|
||||
) : (
|
||||
<EuiText size="s" data-test-subj={MULTI_VALUE_CELL_FIRST_ITEM_VALUE}>
|
||||
{String(items[0])}
|
||||
</EuiText>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{items.length > 1 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiBadge
|
||||
data-test-subj={MULTI_VALUE_CELL_MORE_BUTTON}
|
||||
color="hollow"
|
||||
onClick={onButtonClick}
|
||||
onClickAriaLabel={getShowMoreAriaLabel(field, items.length - 1)}
|
||||
>
|
||||
{`+ ${items.length - 1}`}
|
||||
</EuiBadge>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="s"
|
||||
repositionOnScroll
|
||||
panelStyle={{ minWidth: 'min-content' }}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
wrap={false}
|
||||
responsive={false}
|
||||
gutterSize="xs"
|
||||
justifyContent="flexStart"
|
||||
alignItems="flexStart"
|
||||
css={css`
|
||||
max-height: 230px;
|
||||
overflow-y: auto;
|
||||
max-width: min-content;
|
||||
width: min-content;
|
||||
min-width: unset;
|
||||
padding-right: ${euiTheme.size.s};
|
||||
`}
|
||||
>
|
||||
{items.map((item, index) => renderItem(item, index, field, object))}
|
||||
</EuiFlexGroup>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const MemoizedMultiValueCellPopoverComponent = React.memo(MultiValueCellPopoverComponent);
|
||||
MemoizedMultiValueCellPopoverComponent.displayName = 'MultiValueCellPopover';
|
||||
|
||||
export const MultiValueCellPopover =
|
||||
MemoizedMultiValueCellPopoverComponent as typeof MultiValueCellPopoverComponent;
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export * from './integration-vendors';
|
||||
export * from './test_subjects';
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const MULTI_VALUE_CELL_FIRST_ITEM_VALUE = 'multi_value_cell_first_item_value';
|
||||
export const MULTI_VALUE_CELL_MORE_BUTTON = 'multi_value_cell_more_button';
|
|
@ -30,7 +30,7 @@ export enum VULNERABILITY {
|
|||
TITLE = 'vulnerability.title',
|
||||
ID = 'vulnerability.id',
|
||||
SEVERITY = 'vulnerability.severity',
|
||||
PACKAGE_NAME = 'vulnerability.package.name',
|
||||
PACKAGE_NAME = 'package.name',
|
||||
}
|
||||
|
||||
type LatestFindingsRequest = IKibanaSearchRequest<SearchRequest>;
|
||||
|
|
|
@ -202,7 +202,7 @@ export const VULNERABILITY_GROUPING_OPTIONS = {
|
|||
RESOURCE_ID: VULNERABILITY_FIELDS.RESOURCE_ID,
|
||||
CLOUD_ACCOUNT_NAME: VULNERABILITY_FIELDS.CLOUD_ACCOUNT_NAME,
|
||||
CVE: VULNERABILITY_FIELDS.VULNERABILITY_ID,
|
||||
};
|
||||
} as const;
|
||||
|
||||
/*
|
||||
* ECS schema unique field to describe the event
|
||||
|
|
|
@ -25,3 +25,9 @@ export interface FindingsBaseESQueryConfig {
|
|||
}
|
||||
|
||||
export type Sort<T> = NonNullable<Criteria<T>['sort']>;
|
||||
|
||||
export type VulnerabilityGroupingMultiValueOptions =
|
||||
| 'vulnerability.id'
|
||||
| 'package.name'
|
||||
| 'package.version'
|
||||
| 'package.fixed_version';
|
||||
|
|
|
@ -135,6 +135,7 @@ export const CloudSecurityDataTable = ({
|
|||
columnsLocalStorageKey,
|
||||
defaultColumns.map((c) => c.id)
|
||||
);
|
||||
|
||||
const [persistedSettings, setPersistedSettings] = useLocalStorage<UnifiedDataTableSettings>(
|
||||
`${columnsLocalStorageKey}:settings`,
|
||||
{
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { EuiButtonIcon, EuiCopy } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const COPY_ARIA_LABEL = i18n.translate('xpack.csp.clipboard.copy', {
|
||||
defaultMessage: 'Copy',
|
||||
});
|
||||
|
||||
export const CopyButton: React.FC<{ copyText: string }> = ({ copyText }) => (
|
||||
<EuiCopy textToCopy={copyText}>
|
||||
{(copy) => (
|
||||
<EuiButtonIcon color="text" aria-label={COPY_ARIA_LABEL} iconType="copy" onClick={copy} />
|
||||
)}
|
||||
</EuiCopy>
|
||||
);
|
|
@ -6,25 +6,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiDescriptionList, useEuiTheme, EuiIcon, EuiCopy } from '@elastic/eui';
|
||||
import { EuiDescriptionList, useEuiTheme } from '@elastic/eui';
|
||||
import type { EuiDescriptionListProps } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
const CopyButton = ({ copyText }: { copyText: string }) => (
|
||||
<EuiCopy textToCopy={copyText}>
|
||||
{(copy) => (
|
||||
<EuiIcon
|
||||
css={css`
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`}
|
||||
onClick={copy}
|
||||
type="copy"
|
||||
/>
|
||||
)}
|
||||
</EuiCopy>
|
||||
);
|
||||
import { CopyButton } from './copy_button';
|
||||
|
||||
const getModifiedTitlesListItems = (listItems?: EuiDescriptionListProps['listItems']) =>
|
||||
listItems
|
||||
|
@ -54,7 +38,6 @@ export const CspInlineDescriptionList = ({
|
|||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const modifiedTitlesListItems = getModifiedTitlesListItems(listItems);
|
||||
|
||||
return (
|
||||
<EuiDescriptionList
|
||||
data-test-subj={testId}
|
||||
|
@ -62,15 +45,17 @@ export const CspInlineDescriptionList = ({
|
|||
titleProps={{
|
||||
style: {
|
||||
background: 'initial',
|
||||
color: euiTheme.colors.subduedText,
|
||||
color: euiTheme.colors.textSubdued,
|
||||
fontSize,
|
||||
paddingRight: 0,
|
||||
paddingInline: 0,
|
||||
marginInline: 'unset',
|
||||
marginInlineEnd: euiTheme.size.xs,
|
||||
},
|
||||
}}
|
||||
descriptionProps={{
|
||||
style: {
|
||||
color: euiTheme.colors.subduedText,
|
||||
color: euiTheme.colors.textSubdued,
|
||||
marginRight: euiTheme.size.xs,
|
||||
fontSize,
|
||||
},
|
||||
|
|
|
@ -41,9 +41,9 @@ export const getDefaultQuery = ({
|
|||
});
|
||||
|
||||
export const defaultColumns: CloudSecurityDefaultColumn[] = [
|
||||
{ id: VULNERABILITY_FIELDS.VULNERABILITY_TITLE, width: 160 },
|
||||
{ id: VULNERABILITY_FIELDS.VULNERABILITY_ID, width: 130 },
|
||||
{ id: VULNERABILITY_FIELDS.SCORE_BASE, width: 80 },
|
||||
{ id: VULNERABILITY_FIELDS.VULNERABILITY_TITLE, width: 160 },
|
||||
{ id: VULNERABILITY_FIELDS.VULNERABILITY_ID, width: 180 },
|
||||
{ id: VULNERABILITY_FIELDS.RESOURCE_NAME },
|
||||
{ id: VULNERABILITY_FIELDS.RESOURCE_ID },
|
||||
{ id: VULNERABILITY_FIELDS.SEVERITY, width: 100 },
|
||||
|
|
|
@ -5,19 +5,25 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { get } from 'lodash/fp';
|
||||
import { DataTableRecord } from '@kbn/discover-utils/types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiDataGridCellValueElementProps, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiDataGridCellValueElementProps, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { generateFilters } from '@kbn/data-plugin/public';
|
||||
import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest';
|
||||
import {
|
||||
ActionableBadge,
|
||||
CVSScoreBadge,
|
||||
SeverityStatusBadge,
|
||||
getNormalizedSeverity,
|
||||
MultiValueCellAction,
|
||||
MultiValueCellPopover,
|
||||
findReferenceLink,
|
||||
} from '@kbn/cloud-security-posture';
|
||||
import { getVendorName } from '@kbn/cloud-security-posture/src/utils/get_vendor_name';
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { useLatestVulnerabilitiesTable } from './hooks/use_latest_vulnerabilities_table';
|
||||
import { LATEST_VULNERABILITIES_TABLE } from './test_subjects';
|
||||
import { getDefaultQuery, defaultColumns } from './constants';
|
||||
|
@ -26,6 +32,15 @@ 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';
|
||||
import { FindingsBaseURLQuery } from '../../common/types';
|
||||
import { useKibana } from '../../common/hooks/use_kibana';
|
||||
import { useDataViewContext } from '../../common/contexts/data_view_context';
|
||||
import { usePersistedQuery } from '../../common/hooks/use_cloud_posture_data_table';
|
||||
import { useUrlQuery } from '../../common/hooks/use_url_query';
|
||||
import { EMPTY_VALUE } from '../configurations/findings_flyout/findings_flyout';
|
||||
import { AddFieldFilterHandler } from './types';
|
||||
|
||||
type URLQuery = FindingsBaseURLQuery & Record<string, any>;
|
||||
|
||||
interface LatestVulnerabilitiesTableProps {
|
||||
groupSelectorComponent?: JSX.Element;
|
||||
|
@ -79,71 +94,6 @@ const title = i18n.translate('xpack.csp.findings.latestVulnerabilities.tableRowT
|
|||
defaultMessage: 'Vulnerabilities',
|
||||
});
|
||||
|
||||
const customCellRenderer = (rows: DataTableRecord[]) => ({
|
||||
'vulnerability.score.base': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
|
||||
<CspVulnerabilityFindingRenderer row={rows[rowIndex]}>
|
||||
{({ finding }) => (
|
||||
<CVSScoreBadge
|
||||
score={finding.vulnerability?.score?.base}
|
||||
version={finding.vulnerability?.score?.version}
|
||||
/>
|
||||
)}
|
||||
</CspVulnerabilityFindingRenderer>
|
||||
),
|
||||
'vulnerability.severity': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
|
||||
<CspVulnerabilityFindingRenderer row={rows[rowIndex]}>
|
||||
{({ finding }) => (
|
||||
<SeverityStatusBadge severity={getNormalizedSeverity(finding.vulnerability.severity)} />
|
||||
)}
|
||||
</CspVulnerabilityFindingRenderer>
|
||||
),
|
||||
'observer.vendor': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
|
||||
<CspVulnerabilityFindingRenderer row={rows[rowIndex]}>
|
||||
{({ finding }) => <>{getVendorName(finding) || '-'}</>}
|
||||
</CspVulnerabilityFindingRenderer>
|
||||
),
|
||||
'vulnerability.id': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
|
||||
<CspVulnerabilityFindingRenderer row={rows[rowIndex]}>
|
||||
{({ finding }) => {
|
||||
if (Array.isArray(finding.vulnerability?.id)) {
|
||||
return <>{finding.vulnerability.id.join(', ')}</>;
|
||||
}
|
||||
return <>{finding.vulnerability?.id || '-'}</>;
|
||||
}}
|
||||
</CspVulnerabilityFindingRenderer>
|
||||
),
|
||||
'package.name': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
|
||||
<CspVulnerabilityFindingRenderer row={rows[rowIndex]}>
|
||||
{({ finding }) => {
|
||||
if (Array.isArray(finding.package.name)) {
|
||||
return <>{finding.package.name.join(', ')}</>;
|
||||
}
|
||||
return <>{finding.package.name || '-'}</>;
|
||||
}}
|
||||
</CspVulnerabilityFindingRenderer>
|
||||
),
|
||||
'package.version': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
|
||||
<CspVulnerabilityFindingRenderer row={rows[rowIndex]}>
|
||||
{({ finding }) => {
|
||||
if (Array.isArray(finding.package.version)) {
|
||||
return <>{finding.package.version.join(', ')}</>;
|
||||
}
|
||||
return <>{finding.package.version || '-'}</>;
|
||||
}}
|
||||
</CspVulnerabilityFindingRenderer>
|
||||
),
|
||||
'package.fixed_version': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
|
||||
<CspVulnerabilityFindingRenderer row={rows[rowIndex]}>
|
||||
{({ finding }) => {
|
||||
if (Array.isArray(finding.package.fixed_version)) {
|
||||
return <>{finding.package.fixed_version.join(', ')}</>;
|
||||
}
|
||||
return <>{finding.package.fixed_version || '-'}</>;
|
||||
}}
|
||||
</CspVulnerabilityFindingRenderer>
|
||||
),
|
||||
});
|
||||
|
||||
export const LatestVulnerabilitiesTable = ({
|
||||
groupSelectorComponent,
|
||||
height,
|
||||
|
@ -163,6 +113,138 @@ export const LatestVulnerabilitiesTable = ({
|
|||
createDetectionRuleFromVulnerabilityFinding(http, vulnerabilityFinding);
|
||||
};
|
||||
|
||||
const { data } = useKibana().services;
|
||||
const { filterManager } = data.query;
|
||||
const { dataView } = useDataViewContext();
|
||||
const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery);
|
||||
const { setUrlQuery } = useUrlQuery<URLQuery>(getPersistedDefaultQuery);
|
||||
|
||||
const onAddFilter = useCallback<AddFieldFilterHandler>(
|
||||
(clickedField, value, operation) => {
|
||||
const newFilters = generateFilters(filterManager, clickedField, value, operation, dataView);
|
||||
filterManager.addFilters(newFilters);
|
||||
setUrlQuery({
|
||||
filters: filterManager.getFilters(),
|
||||
});
|
||||
},
|
||||
[dataView, filterManager, setUrlQuery]
|
||||
);
|
||||
|
||||
const renderActionableBadge = useCallback(
|
||||
(item: string, i: number, field: string, object: CspVulnerabilityFinding) => {
|
||||
const references = Array.isArray(object.vulnerability.reference)
|
||||
? object.vulnerability.reference
|
||||
: [object.vulnerability.reference];
|
||||
|
||||
const actions: MultiValueCellAction[] = [
|
||||
{
|
||||
onClick: () => onAddFilter(field, item, '+'),
|
||||
iconType: 'plusInCircle',
|
||||
ariaLabel: i18n.translate('xpack.csp.latestVulnerabilities.table.addFilter', {
|
||||
defaultMessage: 'Add filter',
|
||||
}),
|
||||
title: i18n.translate('xpack.csp.latestVulnerabilities.table.filterForTitle', {
|
||||
defaultMessage: 'Filter for this {value}',
|
||||
values: { value: field },
|
||||
}),
|
||||
},
|
||||
{
|
||||
onClick: () => onAddFilter(field, item, '-'),
|
||||
iconType: 'minusInCircle',
|
||||
ariaLabel: i18n.translate('xpack.csp.latestVulnerabilities.table.removeFilter', {
|
||||
defaultMessage: 'Remove filter',
|
||||
}),
|
||||
title: i18n.translate('xpack.csp.latestVulnerabilities.table.filterOutTitle', {
|
||||
defaultMessage: 'Filter out this {value}',
|
||||
values: { value: field },
|
||||
}),
|
||||
},
|
||||
...(field === 'vulnerability.id' && findReferenceLink(references, item)
|
||||
? [
|
||||
{
|
||||
onClick: () => window.open(findReferenceLink(references, item)!, '_blank'),
|
||||
iconType: 'popout',
|
||||
ariaLabel: i18n.translate('xpack.csp.latestVulnerabilities.table.openUrlInWindow', {
|
||||
defaultMessage: 'Open URL in window',
|
||||
}),
|
||||
title: i18n.translate('xpack.csp.latestVulnerabilities.table.openUrlInWindow', {
|
||||
defaultMessage: 'Open URL in window',
|
||||
}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return <ActionableBadge key={`${item}-${i}`} item={item} index={i} actions={actions} />;
|
||||
},
|
||||
[onAddFilter]
|
||||
);
|
||||
|
||||
const renderMultiValueCell = (field: string, finding: CspVulnerabilityFinding) => {
|
||||
const value = get(field, finding);
|
||||
|
||||
if (!Array.isArray(value)) {
|
||||
return <EuiText size="s">{value || EMPTY_VALUE}</EuiText>;
|
||||
}
|
||||
|
||||
return (
|
||||
<MultiValueCellPopover<CspVulnerabilityFinding>
|
||||
items={value}
|
||||
field={field}
|
||||
object={finding}
|
||||
renderItem={renderActionableBadge}
|
||||
firstItemRenderer={(item) => <EuiText size="s">{item}</EuiText>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const customCellRenderer = (tableRows: DataTableRecord[]) => {
|
||||
return {
|
||||
'vulnerability.score.base': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
|
||||
<CspVulnerabilityFindingRenderer row={tableRows[rowIndex]}>
|
||||
{({ finding }) => (
|
||||
<CVSScoreBadge
|
||||
score={finding.vulnerability?.score?.base}
|
||||
version={finding.vulnerability?.score?.version}
|
||||
/>
|
||||
)}
|
||||
</CspVulnerabilityFindingRenderer>
|
||||
),
|
||||
'vulnerability.severity': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
|
||||
<CspVulnerabilityFindingRenderer row={tableRows[rowIndex]}>
|
||||
{({ finding }) => (
|
||||
<SeverityStatusBadge severity={getNormalizedSeverity(finding.vulnerability.severity)} />
|
||||
)}
|
||||
</CspVulnerabilityFindingRenderer>
|
||||
),
|
||||
'observer.vendor': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
|
||||
<CspVulnerabilityFindingRenderer row={tableRows[rowIndex]}>
|
||||
{({ finding }) => <>{getVendorName(finding) || '-'}</>}
|
||||
</CspVulnerabilityFindingRenderer>
|
||||
),
|
||||
'vulnerability.id': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
|
||||
<CspVulnerabilityFindingRenderer row={tableRows[rowIndex]}>
|
||||
{({ finding }) => renderMultiValueCell('vulnerability.id', finding)}
|
||||
</CspVulnerabilityFindingRenderer>
|
||||
),
|
||||
'package.name': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
|
||||
<CspVulnerabilityFindingRenderer row={rows[rowIndex]}>
|
||||
{({ finding }) => renderMultiValueCell('package.name', finding)}
|
||||
</CspVulnerabilityFindingRenderer>
|
||||
),
|
||||
'package.version': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
|
||||
<CspVulnerabilityFindingRenderer row={rows[rowIndex]}>
|
||||
{({ finding }) => renderMultiValueCell('package.version', finding)}
|
||||
</CspVulnerabilityFindingRenderer>
|
||||
),
|
||||
'package.fixed_version': ({ rowIndex }: EuiDataGridCellValueElementProps) => (
|
||||
<CspVulnerabilityFindingRenderer row={rows[rowIndex]}>
|
||||
{({ finding }) => renderMultiValueCell('package.fixed_version', finding)}
|
||||
</CspVulnerabilityFindingRenderer>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{error ? (
|
||||
|
|
|
@ -19,3 +19,5 @@ export interface Vector {
|
|||
vector: string;
|
||||
score: number | undefined;
|
||||
}
|
||||
|
||||
export type AddFieldFilterHandler = (field: string, value: unknown, type: '+' | '-') => void;
|
||||
|
|
|
@ -160,9 +160,8 @@ describe('<VulnerabilityFindingFlyout/>', () => {
|
|||
getAllByText(mockVulnerabilityHit.vulnerability?.cvss?.redhat?.V3Score?.toString() as string);
|
||||
getAllByText(mockVulnerabilityHit.vulnerability?.cvss?.ghsa?.V3Vector?.toString() as string);
|
||||
getAllByText(mockVulnerabilityHit.vulnerability?.cvss?.ghsa?.V3Score?.toString() as string);
|
||||
getByText(
|
||||
`${mockVulnerabilityHit.package.name} ${mockVulnerabilityHit.package.fixed_version}`
|
||||
);
|
||||
getByText(mockVulnerabilityHit.package.name as string);
|
||||
getByText(mockVulnerabilityHit.package.fixed_version as string);
|
||||
});
|
||||
|
||||
it('Overview Tab with Wiz vulnerability missing fields', () => {
|
||||
|
|
|
@ -31,7 +31,13 @@ import { HttpSetup } from '@kbn/core-http-browser';
|
|||
import type { CspVulnerabilityFinding } from '@kbn/cloud-security-posture-common/schema/vulnerabilities/latest';
|
||||
import { isNativeCspFinding } from '@kbn/cloud-security-posture/src/utils/is_native_csp_finding';
|
||||
import { truthy } from '@kbn/cloud-security-posture/src/utils/helpers';
|
||||
import { SeverityStatusBadge, getNormalizedSeverity } from '@kbn/cloud-security-posture';
|
||||
import {
|
||||
MultiValueCellPopover,
|
||||
SeverityStatusBadge,
|
||||
findReferenceLink,
|
||||
getNormalizedSeverity,
|
||||
} from '@kbn/cloud-security-posture';
|
||||
import { CopyButton } from '../../../components/copy_button';
|
||||
import { TakeAction } from '../../../components/take_action';
|
||||
import { CspInlineDescriptionList } from '../../../components/csp_inline_description_list';
|
||||
import { VulnerabilityOverviewTab } from './vulnerability_overview_tab';
|
||||
|
@ -47,12 +53,31 @@ import {
|
|||
import { VulnerabilityTableTab } from './vulnerability_table_tab';
|
||||
import { createDetectionRuleFromVulnerabilityFinding } from '../utils/create_detection_rule_from_vulnerability';
|
||||
import { MissingFieldsCallout } from '../../configurations/findings_flyout/findings_flyout';
|
||||
import { findReferenceLink } from '../utils/find_reference_link.util';
|
||||
|
||||
const overviewTabId = 'vuln-flyout-overview-tab';
|
||||
const tableTabId = 'vuln-flyout-table-tab';
|
||||
const jsonTabId = 'vuln-flyout-json-tab';
|
||||
|
||||
const renderFinding = (item: string, i: number, field: string, object: CspVulnerabilityFinding) => (
|
||||
<EuiFlexGroup gutterSize="xs" direction="row" justifyContent="flexStart" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiText
|
||||
size="s"
|
||||
css={css`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`}
|
||||
>
|
||||
{item}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<CopyButton copyText={item} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
const getFlyoutDescriptionList = (
|
||||
vulnerabilityRecord: CspVulnerabilityFinding
|
||||
): EuiDescriptionListProps['listItems'] =>
|
||||
|
@ -76,14 +101,44 @@ const getFlyoutDescriptionList = (
|
|||
'xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.packageTitle',
|
||||
{ defaultMessage: 'Package' }
|
||||
),
|
||||
description: vulnerabilityRecord.package.name,
|
||||
description: Array.isArray(vulnerabilityRecord.package?.name) ? (
|
||||
<div
|
||||
css={css`
|
||||
display: inline-block;
|
||||
`}
|
||||
>
|
||||
<MultiValueCellPopover<CspVulnerabilityFinding>
|
||||
items={vulnerabilityRecord.package?.name}
|
||||
field={'package.name'}
|
||||
object={vulnerabilityRecord}
|
||||
renderItem={renderFinding}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
vulnerabilityRecord.package?.name
|
||||
),
|
||||
},
|
||||
vulnerabilityRecord.package?.version && {
|
||||
title: i18n.translate(
|
||||
'xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.versionTitle',
|
||||
{ defaultMessage: 'Version' }
|
||||
),
|
||||
description: vulnerabilityRecord.package.version,
|
||||
description: Array.isArray(vulnerabilityRecord.package?.version) ? (
|
||||
<div
|
||||
css={css`
|
||||
display: inline-block;
|
||||
`}
|
||||
>
|
||||
<MultiValueCellPopover<CspVulnerabilityFinding>
|
||||
items={vulnerabilityRecord.package?.version}
|
||||
field={'package.version'}
|
||||
object={vulnerabilityRecord}
|
||||
renderItem={renderFinding}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
vulnerabilityRecord.package?.version
|
||||
),
|
||||
},
|
||||
].filter(truthy);
|
||||
|
||||
|
|
|
@ -29,8 +29,13 @@ import {
|
|||
VULNERABILITIES_FLYOUT_VISITS,
|
||||
uiMetricService,
|
||||
} from '@kbn/cloud-security-posture-common/utils/ui_metrics';
|
||||
import { CVSScoreBadge } from '@kbn/cloud-security-posture';
|
||||
import {
|
||||
CVSScoreBadge,
|
||||
MultiValueCellPopover,
|
||||
findReferenceLink,
|
||||
} from '@kbn/cloud-security-posture';
|
||||
import { getVendorName } from '@kbn/cloud-security-posture/src/utils/get_vendor_name';
|
||||
import { get } from 'lodash/fp';
|
||||
import { CspFlyoutMarkdown } from '../../configurations/findings_flyout/findings_flyout';
|
||||
import { NvdLogo } from '../../../assets/icons/nvd_logo_svg';
|
||||
import { CVSScoreProps, Vendor } from '../types';
|
||||
|
@ -47,7 +52,7 @@ import {
|
|||
} from '../test_subjects';
|
||||
import redhatLogo from '../../../assets/icons/redhat_logo.svg';
|
||||
import { VulnerabilityDetectionRuleCounter } from './vulnerability_detection_rule_counter';
|
||||
import { findReferenceLink } from '../utils/find_reference_link.util';
|
||||
import { CopyButton } from '../../../components/copy_button';
|
||||
|
||||
const cvssVendors: Record<string, Vendor> = {
|
||||
nvd: 'NVD',
|
||||
|
@ -217,9 +222,109 @@ export const VulnerabilityOverviewTab = ({ vulnerabilityRecord }: VulnerabilityT
|
|||
}
|
||||
);
|
||||
|
||||
const fixesDisplayText = vulnerabilityRecord?.package?.fixed_version
|
||||
? `${vulnerabilityRecord?.package?.name} ${vulnerabilityRecord?.package?.fixed_version}`
|
||||
: emptyFixesMessageState;
|
||||
const renderFixedBySection = () => {
|
||||
const renderFinding = (
|
||||
item: string,
|
||||
i: number,
|
||||
field: string,
|
||||
object: CspVulnerabilityFinding
|
||||
) => (
|
||||
<EuiFlexGroup gutterSize="xs" direction="row" justifyContent="flexStart" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiText
|
||||
size="s"
|
||||
css={css`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`}
|
||||
>
|
||||
{item}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<CopyButton copyText={item} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
const renderPackageField = (field: string) => {
|
||||
const value = get(field, vulnerabilityRecord);
|
||||
if (!value) {
|
||||
return <EuiText size="xs">{EMPTY_VALUE}</EuiText>;
|
||||
}
|
||||
|
||||
return Array.isArray(value) ? (
|
||||
<MultiValueCellPopover<CspVulnerabilityFinding>
|
||||
items={value}
|
||||
field={`package.${field}`}
|
||||
object={vulnerabilityRecord}
|
||||
renderItem={renderFinding}
|
||||
/>
|
||||
) : (
|
||||
value
|
||||
);
|
||||
};
|
||||
|
||||
if (!vulnerabilityRecord?.package?.fixed_version) {
|
||||
return <EuiText>{emptyFixesMessageState}</EuiText>;
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup responsive={false} gutterSize="xs" alignItems="flexStart" direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
gutterSize="xs"
|
||||
justifyContent="flexStart"
|
||||
alignItems="center"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
size="xs"
|
||||
css={css`
|
||||
font-weight: ${euiTheme.font.weight.bold};
|
||||
`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.packageTitle"
|
||||
defaultMessage="Package"
|
||||
/>
|
||||
:
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{renderPackageField('package.name')}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
gutterSize="xs"
|
||||
justifyContent="flexStart"
|
||||
alignItems="center"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText
|
||||
size="xs"
|
||||
css={css`
|
||||
font-weight: ${euiTheme.font.weight.bold};
|
||||
`}
|
||||
>
|
||||
<FormattedMessage
|
||||
css={css`
|
||||
font-weight: ${euiTheme.font.weight.medium};
|
||||
`}
|
||||
id="xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.versionTitle"
|
||||
defaultMessage="Version"
|
||||
/>
|
||||
:
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{renderPackageField('package.fixed_version')}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const cvssScores = vulnerability?.cvss
|
||||
? Object.entries<VectorScoreBase>(vulnerability.cvss).map(
|
||||
|
@ -436,7 +541,7 @@ export const VulnerabilityOverviewTab = ({ vulnerabilityRecord }: VulnerabilityT
|
|||
defaultMessage="Fixed by"
|
||||
/>
|
||||
</h4>
|
||||
<EuiText>{fixesDisplayText || EMPTY_VALUE}</EuiText>
|
||||
{renderFixedBySection()}
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiHorizontalRule css={horizontalStyle} />
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useState } from 'react';
|
||||
import type { Criteria, EuiBasicTableColumn, EuiTableSortingType } from '@elastic/eui';
|
||||
import { EuiSpacer, EuiPanel, EuiText, EuiBasicTable, EuiIcon } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -19,11 +19,15 @@ import {
|
|||
useVulnerabilitiesFindings,
|
||||
VULNERABILITY,
|
||||
} from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_findings';
|
||||
import type { MultiValueCellAction } from '@kbn/cloud-security-posture';
|
||||
import {
|
||||
getVulnerabilityStats,
|
||||
CVSScoreBadge,
|
||||
SeverityStatusBadge,
|
||||
getNormalizedSeverity,
|
||||
ActionableBadge,
|
||||
MultiValueCellPopover,
|
||||
findReferenceLink,
|
||||
} from '@kbn/cloud-security-posture';
|
||||
import {
|
||||
ENTITY_FLYOUT_EXPAND_VULNERABILITY_VIEW_VISITS,
|
||||
|
@ -35,6 +39,7 @@ import { SecurityPageName } from '@kbn/deeplinks-security';
|
|||
import { useGetNavigationUrlParams } from '@kbn/cloud-security-posture/src/hooks/use_get_navigation_url_params';
|
||||
import { useGetSeverityStatusColor } from '@kbn/cloud-security-posture/src/hooks/use_get_severity_status_color';
|
||||
import { useHasVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_has_vulnerabilities';
|
||||
import { get } from 'lodash/fp';
|
||||
import { EntityIdentifierFields } from '../../../../common/entity_analytics/types';
|
||||
import { SecuritySolutionLinkAnchor } from '../../../common/components/links';
|
||||
import type { CloudPostureEntityIdentifier } from '../entity_insight';
|
||||
|
@ -163,6 +168,59 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
|
|||
currentFilter
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
(item: string, i: number, field: string, object: VulnerabilitiesFindingDetailFields) => {
|
||||
const references = Array.isArray(object.vulnerability.reference)
|
||||
? object.vulnerability.reference
|
||||
: [object.vulnerability.reference];
|
||||
|
||||
const url = findReferenceLink(references, item);
|
||||
|
||||
const actions: MultiValueCellAction[] = [
|
||||
...(field === 'vulnerability.id' && url
|
||||
? [
|
||||
{
|
||||
onClick: () => window.open(url, '_blank'),
|
||||
iconType: 'popout',
|
||||
ariaLabel: i18n.translate(
|
||||
'xpack.securitySolution.vulnerabilities.findingsDetailsTable.openUrlInWindow',
|
||||
{
|
||||
defaultMessage: 'Open URL in window',
|
||||
}
|
||||
),
|
||||
title: i18n.translate(
|
||||
'xpack.securitySolution.vulnerabilities.findingsDetailsTable.openUrlInWindow',
|
||||
{
|
||||
defaultMessage: 'Open URL in window',
|
||||
}
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return <ActionableBadge key={`${item}-${i}`} item={item} index={i} actions={actions} />;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const renderMultiValueCell = (field: string, finding: VulnerabilitiesFindingDetailFields) => {
|
||||
const cellValue = get(field, finding);
|
||||
if (!Array.isArray(cellValue)) {
|
||||
return <EuiText size="s">{cellValue || EMPTY_VALUE}</EuiText>;
|
||||
}
|
||||
|
||||
return (
|
||||
<MultiValueCellPopover<VulnerabilitiesFindingDetailFields>
|
||||
items={cellValue}
|
||||
field={field}
|
||||
object={finding}
|
||||
renderItem={renderItem}
|
||||
firstItemRenderer={(item) => <EuiText size="s">{item}</EuiText>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<VulnerabilitiesFindingDetailFields>> = [
|
||||
{
|
||||
field: 'vulnerability',
|
||||
|
@ -187,6 +245,20 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
|
|||
</SecuritySolutionLinkAnchor>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'score',
|
||||
render: (score: { version?: string; base?: number }) => (
|
||||
<EuiText size="s">
|
||||
<CVSScoreBadge version={score?.version} score={score?.base} />
|
||||
</EuiText>
|
||||
),
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.vulnerability.table.ruleColumnName',
|
||||
{ defaultMessage: 'CVSS' }
|
||||
),
|
||||
width: '10%',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: VULNERABILITY.TITLE,
|
||||
render: (title: string) => {
|
||||
|
@ -205,13 +277,8 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
|
|||
},
|
||||
{
|
||||
field: VULNERABILITY.ID,
|
||||
render: (id: string) => {
|
||||
if (Array.isArray(id)) {
|
||||
return <EuiText size="s">{id.join(', ')}</EuiText>;
|
||||
}
|
||||
|
||||
return <EuiText size="s">{id || EMPTY_VALUE}</EuiText>;
|
||||
},
|
||||
render: (id: string, finding: VulnerabilitiesFindingDetailFields) =>
|
||||
renderMultiValueCell(VULNERABILITY.ID, finding),
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.vulnerability.table.vulnerabilityIdColumnName',
|
||||
{ defaultMessage: 'CVE ID' }
|
||||
|
@ -219,20 +286,6 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
|
|||
width: '20%',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'score',
|
||||
render: (score: { version?: string; base?: number }) => (
|
||||
<EuiText size="s">
|
||||
<CVSScoreBadge version={score?.version} score={score?.base} />
|
||||
</EuiText>
|
||||
),
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.vulnerability.table.ruleColumnName',
|
||||
{ defaultMessage: 'CVSS' }
|
||||
),
|
||||
width: '10%',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: VULNERABILITY.SEVERITY,
|
||||
render: (severity: string) => (
|
||||
|
@ -251,7 +304,8 @@ export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: str
|
|||
},
|
||||
{
|
||||
field: VULNERABILITY.PACKAGE_NAME,
|
||||
render: (packageName: string) => <EuiText size="s">{packageName}</EuiText>,
|
||||
render: (packageName: string, finding: VulnerabilitiesFindingDetailFields) =>
|
||||
renderMultiValueCell(VULNERABILITY.PACKAGE_NAME, finding),
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.vulnerability.table.ruleColumnName',
|
||||
{ defaultMessage: 'Package' }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue