mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Cloud Security] Allow open Vulnerability Flyout from Contextual Flyout (#221219)
## Summary Users now are able to open Vulnerability Findings Flyout via Contextual Flyout * This PR also removes the Pivoting from title for both Misconfiguration and Vulnerabilities Flyout on Alerts page https://github.com/user-attachments/assets/6aa20a10-ae98-4856-b871-90acfca4aeb1
This commit is contained in:
parent
4c89a9ac50
commit
8c811f5e0f
17 changed files with 505 additions and 402 deletions
|
@ -244,7 +244,9 @@ describe('test helper methods', () => {
|
|||
},
|
||||
{
|
||||
bool: {
|
||||
should: [{ term: { 'some.field': 'some.status' } }],
|
||||
should: [
|
||||
{ term: { 'some.field': { value: 'some.status', case_insensitive: true } } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
|
@ -274,7 +276,9 @@ describe('test helper methods', () => {
|
|||
},
|
||||
{
|
||||
bool: {
|
||||
should: [{ term: { 'result.evaluation': 'pass' } }],
|
||||
should: [
|
||||
{ term: { 'result.evaluation': { value: 'pass', case_insensitive: true } } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
|
@ -303,7 +307,9 @@ describe('test helper methods', () => {
|
|||
},
|
||||
{
|
||||
bool: {
|
||||
should: [{ term: { 'vulnerability.severity': 'low' } }],
|
||||
should: [
|
||||
{ term: { 'vulnerability.severity': { value: 'low', case_insensitive: true } } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -84,7 +84,10 @@ export const buildGenericEntityFlyoutPreviewQuery = (
|
|||
should: [
|
||||
{
|
||||
term: {
|
||||
[queryField]: status,
|
||||
[queryField]: {
|
||||
value: status,
|
||||
case_insensitive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -53,7 +53,7 @@ export interface VulnerabilitiesFindingTableDetailsFields {
|
|||
}
|
||||
|
||||
export type VulnerabilitiesFindingDetailFields = Pick<Vulnerability, 'score'> &
|
||||
Pick<CspVulnerabilityFinding, 'vulnerability' | 'resource'> &
|
||||
Pick<CspVulnerabilityFinding, 'vulnerability' | 'resource' | 'event'> &
|
||||
VulnerabilitiesFindingTableDetailsFields;
|
||||
|
||||
interface FindingsAggs {
|
||||
|
@ -91,6 +91,7 @@ export const useVulnerabilitiesFindings = (options: UseCspOptions) => {
|
|||
[VULNERABILITY.ID]: finding._source?.vulnerability?.id,
|
||||
[VULNERABILITY.SEVERITY]: finding._source?.vulnerability?.severity,
|
||||
[VULNERABILITY.PACKAGE_NAME]: finding._source?.package?.name,
|
||||
event: finding._source?.event,
|
||||
})) as VulnerabilitiesFindingDetailFields[],
|
||||
};
|
||||
},
|
||||
|
|
|
@ -85,7 +85,7 @@ export interface FindingsAggs {
|
|||
count: estypes.AggregationsMultiBucketAggregateBase<estypes.AggregationsStringRareTermsBucketKeys>;
|
||||
}
|
||||
|
||||
interface BaseFlyoutProps {
|
||||
interface BaseMisconfigurationFlyoutProps {
|
||||
ruleId: string;
|
||||
resourceId: string;
|
||||
}
|
||||
|
@ -105,12 +105,12 @@ interface NonPreviewModeProps {
|
|||
}
|
||||
export type FindingsMisconfigurationPanelExpandableFlyoutPropsNonPreview = FlyoutPanelProps & {
|
||||
id: 'findings-misconfiguration-panel';
|
||||
params: BaseFlyoutProps & NonPreviewModeProps;
|
||||
params: BaseMisconfigurationFlyoutProps & NonPreviewModeProps;
|
||||
};
|
||||
|
||||
export type FindingsMisconfigurationPanelExpandableFlyoutPropsPreview = FlyoutPanelProps & {
|
||||
id: 'findings-misconfiguration-panel-preview';
|
||||
params: BaseFlyoutProps & PreviewModeProps;
|
||||
params: BaseMisconfigurationFlyoutProps & PreviewModeProps;
|
||||
};
|
||||
|
||||
export type FindingsMisconfigurationPanelExpandableFlyoutProps =
|
||||
|
@ -143,10 +143,6 @@ export interface FindingVulnerabilityFlyoutProps extends Record<string, unknown>
|
|||
packageVersion: string | string[];
|
||||
eventId: string;
|
||||
}
|
||||
export interface FindingVulnerabilityPanelExpandableFlyoutProps extends FlyoutPanelProps {
|
||||
key: 'findings-vulnerability-panel';
|
||||
params: FindingVulnerabilityFlyoutProps;
|
||||
}
|
||||
|
||||
export interface FindingsVulnerabilityFlyoutHeaderProps {
|
||||
finding: CspVulnerabilityFinding;
|
||||
|
@ -154,6 +150,7 @@ export interface FindingsVulnerabilityFlyoutHeaderProps {
|
|||
|
||||
export interface FindingsVulnerabilityFlyoutContentProps {
|
||||
finding: CspVulnerabilityFinding;
|
||||
isPreviewMode?: boolean;
|
||||
}
|
||||
|
||||
export interface FindingsVulnerabilityFlyoutFooterProps {
|
||||
|
@ -163,4 +160,27 @@ export interface FindingsVulnerabilityFlyoutFooterProps {
|
|||
export interface FindingVulnerabilityFullFlyoutContentProps {
|
||||
finding: CspVulnerabilityFinding;
|
||||
createRuleFn: (http: HttpSetup) => Promise<RuleResponse>;
|
||||
isPreviewMode?: boolean;
|
||||
}
|
||||
|
||||
interface BaseVulnerabilityFlyoutProps {
|
||||
vulnerabilityId: string | string[];
|
||||
resourceId: string;
|
||||
packageName: string | string[];
|
||||
packageVersion: string | string[];
|
||||
eventId: string;
|
||||
}
|
||||
|
||||
export type FindingsVulnerabilityPanelExpandableFlyoutPropsNonPreview = FlyoutPanelProps & {
|
||||
id: 'findings-vulnerability-panel';
|
||||
params: BaseVulnerabilityFlyoutProps & NonPreviewModeProps;
|
||||
};
|
||||
|
||||
export type FindingsVulnerabilityPanelExpandableFlyoutPropsPreview = FlyoutPanelProps & {
|
||||
id: 'findings-vulnerability-panel-preview';
|
||||
params: BaseVulnerabilityFlyoutProps & PreviewModeProps;
|
||||
};
|
||||
|
||||
export type FindingsVulnerabilityPanelExpandableFlyoutProps =
|
||||
| FindingsVulnerabilityPanelExpandableFlyoutPropsNonPreview
|
||||
| FindingsVulnerabilityPanelExpandableFlyoutPropsPreview;
|
||||
|
|
|
@ -145,7 +145,9 @@ const getDetailsList = (
|
|||
<>
|
||||
<EuiFlexGroup direction="row" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<b>Rule Description</b>
|
||||
{i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.ruleDescription', {
|
||||
defaultMessage: 'Rule Description',
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiLink href={ruleFlyoutLink} target="_blank" css={{ textAlign: 'right' }}>
|
||||
|
|
|
@ -17,7 +17,6 @@ import {
|
|||
VULNERABILITY_HEADER_CVE_BADGE,
|
||||
VULNERABILITY_HEADER_ID,
|
||||
VULNERABILITY_HEADER_REFERENCE_LINK,
|
||||
VULNERABILITY_HEADER_TITLE,
|
||||
VULNERABILITY_OVERVIEW_TAB_ID_LESS_BTN,
|
||||
VULNERABILITY_OVERVIEW_TAB_ID_MORE_BTN,
|
||||
VULNERABILITY_SCORES_FLYOUT,
|
||||
|
@ -69,16 +68,12 @@ describe('<VulnerabilityFindingFlyout/>', () => {
|
|||
getAllByText(mockVulnerabilityHit.vulnerability.id as string);
|
||||
});
|
||||
|
||||
it('displays title and reference link for the matching id', () => {
|
||||
it('displays reference link for the matching id', () => {
|
||||
(useVulnerabilityFinding as jest.Mock).mockReturnValue({
|
||||
data: { result: { hits: [{ _source: mockVulnerabilityHit }] } },
|
||||
});
|
||||
const { getByTestId } = render(<TestComponent />);
|
||||
|
||||
expect(getByTestId(VULNERABILITY_HEADER_TITLE).textContent).toEqual(
|
||||
mockVulnerabilityHit.vulnerability.title
|
||||
);
|
||||
|
||||
const idLinkElement = getByTestId(VULNERABILITY_HEADER_REFERENCE_LINK);
|
||||
expect(idLinkElement.textContent).toMatch(
|
||||
new RegExp(`^${mockVulnerabilityHit.vulnerability.id}`)
|
||||
|
@ -97,10 +92,6 @@ describe('<VulnerabilityFindingFlyout/>', () => {
|
|||
});
|
||||
const { getByTestId } = render(<TestComponent />);
|
||||
|
||||
expect(getByTestId(VULNERABILITY_HEADER_TITLE).textContent).toEqual(
|
||||
mockQualysVulnerabilityHit.vulnerability.title
|
||||
);
|
||||
|
||||
const idLinkElement = getByTestId(VULNERABILITY_HEADER_REFERENCE_LINK);
|
||||
expect(idLinkElement.textContent).toMatch(
|
||||
new RegExp(`^${mockQualysVulnerabilityHit.vulnerability.id[0]}`)
|
||||
|
@ -137,10 +128,6 @@ describe('<VulnerabilityFindingFlyout/>', () => {
|
|||
});
|
||||
const { getByTestId, queryByTestId } = render(<TestComponent />);
|
||||
|
||||
expect(getByTestId(VULNERABILITY_HEADER_TITLE).textContent).toEqual(
|
||||
mockQualysVulnerabilityHit.vulnerability.title
|
||||
);
|
||||
|
||||
const idLinkElement = queryByTestId(VULNERABILITY_HEADER_REFERENCE_LINK);
|
||||
expect(idLinkElement).toBeNull();
|
||||
|
||||
|
|
|
@ -13,53 +13,26 @@ import {
|
|||
VULNERABILITY_HEADER_CVE_BADGE,
|
||||
VULNERABILITY_HEADER_ID,
|
||||
VULNERABILITY_HEADER_REFERENCE_LINK,
|
||||
VULNERABILITY_HEADER_TITLE,
|
||||
} from '../../test_subjects';
|
||||
import { TestProvider } from '../../../../test/test_provider';
|
||||
|
||||
describe('FindingsVulnerabilityFlyoutHeader', () => {
|
||||
it('renders the component and displays the vulnerability title and CVEs', () => {
|
||||
it('renders the component and displays the vulnerability CVEs', () => {
|
||||
render(
|
||||
<TestProvider>
|
||||
<FindingsVulnerabilityFlyoutHeader finding={mockQualysVulnerabilityHit} />
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
const titleElement = screen.getByTestId(VULNERABILITY_HEADER_TITLE);
|
||||
const cveslinkElement = screen.getByTestId(VULNERABILITY_HEADER_REFERENCE_LINK);
|
||||
const cvesBadge = screen.getByTestId(VULNERABILITY_HEADER_CVE_BADGE);
|
||||
|
||||
expect(titleElement).toBeInTheDocument();
|
||||
expect(titleElement).toHaveTextContent(mockQualysVulnerabilityHit.vulnerability.title);
|
||||
expect(cveslinkElement).toHaveTextContent(mockQualysVulnerabilityHit.vulnerability.id[0]);
|
||||
expect(cvesBadge).toHaveTextContent(
|
||||
`${mockQualysVulnerabilityHit.vulnerability.id.length - 1} More`
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the component and display only CVEs when vulnerability.title is empty', () => {
|
||||
const vulnerabilityHitWithEmptyTitle = {
|
||||
...mockQualysVulnerabilityHit,
|
||||
vulnerability: { ...mockQualysVulnerabilityHit.vulnerability, title: '' },
|
||||
};
|
||||
render(
|
||||
<TestProvider>
|
||||
<FindingsVulnerabilityFlyoutHeader finding={vulnerabilityHitWithEmptyTitle} />
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
const titleElement = screen.getByTestId(VULNERABILITY_HEADER_TITLE);
|
||||
const cvesElement = screen.getByTestId(VULNERABILITY_HEADER_REFERENCE_LINK);
|
||||
const cvesBadge = screen.getByTestId(VULNERABILITY_HEADER_CVE_BADGE);
|
||||
|
||||
expect(titleElement).toBeInTheDocument();
|
||||
expect(titleElement).not.toHaveTextContent(mockQualysVulnerabilityHit.vulnerability.title);
|
||||
expect(cvesElement).toHaveTextContent(vulnerabilityHitWithEmptyTitle.vulnerability.id[0]);
|
||||
expect(cvesBadge).toHaveTextContent(
|
||||
`${vulnerabilityHitWithEmptyTitle.vulnerability.id.length - 1} More`
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the component and display first CVE as text if there is no reference', () => {
|
||||
const vulnerabilityHitWithNoReference = {
|
||||
...mockQualysVulnerabilityHit,
|
||||
|
@ -80,7 +53,7 @@ describe('FindingsVulnerabilityFlyoutHeader', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('renders the component and display onlt CVE without the badge', () => {
|
||||
it('renders the component and display only CVE without the badge', () => {
|
||||
const vulnerabilityHitWithSingleCVE = {
|
||||
...mockQualysVulnerabilityHit,
|
||||
vulnerability: {
|
||||
|
|
|
@ -35,7 +35,6 @@ import {
|
|||
VULNERABILITY_HEADER_CVE_BADGE,
|
||||
VULNERABILITY_HEADER_REFERENCE_LINK,
|
||||
VULNERABILITY_HEADER_ID,
|
||||
VULNERABILITY_HEADER_TITLE,
|
||||
DATA_SOURCE_VULNERABILITY_FLYOUT,
|
||||
VULNERABILITY_SCORES_FLYOUT,
|
||||
} from '../../test_subjects';
|
||||
|
@ -144,11 +143,6 @@ export const FindingsVulnerabilityFlyoutHeader = ({
|
|||
gap: ${euiTheme.size.s};
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="m" css={css``}>
|
||||
<EuiText data-test-subj={VULNERABILITY_HEADER_TITLE}>{vulnerability?.title}</EuiText>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{renderCves()}
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
|
|
|
@ -17,8 +17,8 @@ import type {
|
|||
FindingsVulnerabilityFlyoutContentProps,
|
||||
FindingsVulnerabilityFlyoutFooterProps,
|
||||
FindingsVulnerabilityFlyoutHeaderProps,
|
||||
FindingVulnerabilityFlyoutProps,
|
||||
FindingsMisconfigurationPanelExpandableFlyoutProps,
|
||||
FindingsVulnerabilityPanelExpandableFlyoutProps,
|
||||
} from '@kbn/cloud-security-posture';
|
||||
import { uiMetricService } from '@kbn/cloud-security-posture-common/utils/ui_metrics';
|
||||
import { CspLoadingState } from './components/csp_loading_state';
|
||||
|
@ -169,7 +169,7 @@ export class CspPlugin
|
|||
},
|
||||
getCloudSecurityPostureVulnerabilityFlyout: () => {
|
||||
return {
|
||||
Component: (props: FindingVulnerabilityFlyoutProps) => (
|
||||
Component: (props: FindingsVulnerabilityPanelExpandableFlyoutProps['params']) => (
|
||||
<LazyCspFindingsVulnerabilityFlyout {...props}>
|
||||
{props.children}
|
||||
</LazyCspFindingsVulnerabilityFlyout>
|
||||
|
|
|
@ -16,13 +16,14 @@ import { ExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
|||
import {
|
||||
FindingMisconfigurationFlyoutContentProps,
|
||||
FindingMisconfigurationFlyoutFooterProps,
|
||||
FindingVulnerabilityFlyoutProps,
|
||||
FindingsVulnerabilityFlyoutContentProps,
|
||||
FindingsVulnerabilityFlyoutFooterProps,
|
||||
FindingsVulnerabilityFlyoutHeaderProps,
|
||||
FindingsMisconfigurationFlyoutContentProps,
|
||||
FindingsMisconfigurationFlyoutHeaderProps,
|
||||
FindingsMisconfigurationPanelExpandableFlyoutProps,
|
||||
FindingsVulnerabilityPanelExpandableFlyoutProps,
|
||||
FindingVulnerabilityFullFlyoutContentProps,
|
||||
} from '@kbn/cloud-security-posture';
|
||||
import type { CspRouterProps } from './application/csp_router';
|
||||
import type { CloudSecurityPosturePageId } from './common/navigation/types';
|
||||
|
@ -57,7 +58,11 @@ export interface CspClientPluginStart {
|
|||
Footer: React.FC<FindingMisconfigurationFlyoutFooterProps>;
|
||||
};
|
||||
getCloudSecurityPostureVulnerabilityFlyout: () => {
|
||||
Component: React.FC<FindingVulnerabilityFlyoutProps>;
|
||||
Component: React.FC<
|
||||
FindingsVulnerabilityPanelExpandableFlyoutProps['params'] & {
|
||||
children?: (props: FindingVulnerabilityFullFlyoutContentProps) => ReactNode;
|
||||
}
|
||||
>;
|
||||
Header: React.FC<FindingsVulnerabilityFlyoutHeaderProps>;
|
||||
Body: React.FC<FindingsVulnerabilityFlyoutContentProps>;
|
||||
Footer: React.FC<FindingsVulnerabilityFlyoutFooterProps>;
|
||||
|
|
|
@ -166,7 +166,7 @@ export const InsightsTabCsp = memo(
|
|||
{activeInsightsId === CspInsightLeftPanelSubTab.MISCONFIGURATIONS ? (
|
||||
<MisconfigurationFindingsDetailsTable field={field} value={value} scopeId={scopeId} />
|
||||
) : activeInsightsId === CspInsightLeftPanelSubTab.VULNERABILITIES ? (
|
||||
<VulnerabilitiesFindingsDetailsTable value={value} />
|
||||
<VulnerabilitiesFindingsDetailsTable value={value} scopeId={scopeId} />
|
||||
) : (
|
||||
<AlertsDetailsTable field={field} value={value} />
|
||||
)}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
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 { EuiSpacer, EuiPanel, EuiText, EuiBasicTable, EuiIcon, EuiButtonIcon } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { buildVulnerabilityEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common';
|
||||
import { DistributionBar } from '@kbn/security-solution-distribution-bar';
|
||||
|
@ -19,7 +19,10 @@ import {
|
|||
useVulnerabilitiesFindings,
|
||||
VULNERABILITY,
|
||||
} from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_findings';
|
||||
import type { MultiValueCellAction } from '@kbn/cloud-security-posture';
|
||||
import type {
|
||||
FindingsVulnerabilityPanelExpandableFlyoutPropsPreview,
|
||||
MultiValueCellAction,
|
||||
} from '@kbn/cloud-security-posture';
|
||||
import {
|
||||
getVulnerabilityStats,
|
||||
CVSScoreBadge,
|
||||
|
@ -40,6 +43,9 @@ import { useGetNavigationUrlParams } from '@kbn/cloud-security-posture/src/hooks
|
|||
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 { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
|
||||
import { VulnerabilityFindingsPreviewPanelKey } from '../../../flyout/csp_details/vulnerabilities_flyout/constants';
|
||||
import { EntityIdentifierFields } from '../../../../common/entity_analytics/types';
|
||||
import { SecuritySolutionLinkAnchor } from '../../../common/components/links';
|
||||
import type { CloudPostureEntityIdentifier } from '../entity_insight';
|
||||
|
@ -51,305 +57,316 @@ type VulnerabilitySortFieldType =
|
|||
| VULNERABILITY.SEVERITY
|
||||
| VULNERABILITY.ID
|
||||
| VULNERABILITY.PACKAGE_NAME
|
||||
| VULNERABILITY.TITLE;
|
||||
| VULNERABILITY.TITLE
|
||||
| 'event';
|
||||
|
||||
const EMPTY_VALUE = '-';
|
||||
|
||||
export const VulnerabilitiesFindingsDetailsTable = memo(({ value }: { value: string }) => {
|
||||
const { getSeverityStatusColor } = useGetSeverityStatusColor();
|
||||
export const VulnerabilitiesFindingsDetailsTable = memo(
|
||||
({ value, scopeId }: { value: string; scopeId: string }) => {
|
||||
const { getSeverityStatusColor } = useGetSeverityStatusColor();
|
||||
|
||||
useEffect(() => {
|
||||
uiMetricService.trackUiMetric(
|
||||
METRIC_TYPE.COUNT,
|
||||
ENTITY_FLYOUT_EXPAND_VULNERABILITY_VIEW_VISITS
|
||||
);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
uiMetricService.trackUiMetric(
|
||||
METRIC_TYPE.COUNT,
|
||||
ENTITY_FLYOUT_EXPAND_VULNERABILITY_VIEW_VISITS
|
||||
);
|
||||
}, []);
|
||||
|
||||
const [currentFilter, setCurrentFilter] = useState<string>('');
|
||||
const [sortField, setSortField] = useState<VulnerabilitySortFieldType>(VULNERABILITY.SEVERITY);
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
const [currentFilter, setCurrentFilter] = useState<string>('');
|
||||
const [sortField, setSortField] = useState<VulnerabilitySortFieldType>(VULNERABILITY.SEVERITY);
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
|
||||
const sortFieldDirection: { [key: string]: string } = {};
|
||||
sortFieldDirection[sortField === 'score' ? 'vulnerability.score.base' : sortField] =
|
||||
sortDirection;
|
||||
const { openPreviewPanel } = useExpandableFlyoutApi();
|
||||
|
||||
const sorting: EuiTableSortingType<VulnerabilitiesFindingDetailFields> = {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
const sortFieldDirection: { [key: string]: string } = {};
|
||||
sortFieldDirection[sortField === 'score' ? 'vulnerability.score.base' : sortField] =
|
||||
sortDirection;
|
||||
|
||||
const { data } = useVulnerabilitiesFindings({
|
||||
query: buildVulnerabilityEntityFlyoutPreviewQuery('host.name', value, currentFilter),
|
||||
sort: [sortFieldDirection],
|
||||
enabled: true,
|
||||
pageSize: 1,
|
||||
});
|
||||
|
||||
const { counts } = useHasVulnerabilities('host.name', value);
|
||||
|
||||
const { critical = 0, high = 0, medium = 0, low = 0, none = 0 } = counts || {};
|
||||
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const findingsPagination = (findings: VulnerabilitiesFindingDetailFields[]) => {
|
||||
let pageOfItems;
|
||||
|
||||
if (!pageIndex && !pageSize) {
|
||||
pageOfItems = findings;
|
||||
} else {
|
||||
const startIndex = pageIndex * pageSize;
|
||||
pageOfItems = findings?.slice(startIndex, Math.min(startIndex + pageSize, findings?.length));
|
||||
}
|
||||
|
||||
return {
|
||||
pageOfItems,
|
||||
totalItemCount: findings?.length,
|
||||
};
|
||||
};
|
||||
|
||||
const { pageOfItems, totalItemCount } = findingsPagination(data?.rows || []);
|
||||
|
||||
const pagination = {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount,
|
||||
pageSizeOptions: [10, 25, 100],
|
||||
};
|
||||
|
||||
const onTableChange = ({ page, sort }: Criteria<VulnerabilitiesFindingDetailFields>) => {
|
||||
if (page) {
|
||||
const { index, size } = page;
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
}
|
||||
if (sort) {
|
||||
const { field: fieldSort, direction } = sort;
|
||||
setSortField(fieldSort);
|
||||
setSortDirection(direction);
|
||||
}
|
||||
};
|
||||
|
||||
const getNavUrlParams: ReturnType<typeof useGetNavigationUrlParams> = useGetNavigationUrlParams();
|
||||
|
||||
const getVulnerabilityUrl = (name: string, queryField: CloudPostureEntityIdentifier) => {
|
||||
return getNavUrlParams({ [queryField]: name }, 'vulnerabilities');
|
||||
};
|
||||
|
||||
const getVulnerabilityUrlFilteredByVulnerabilityAndResourceId = (
|
||||
vulnerabilityId: string | string[],
|
||||
resourceId: string,
|
||||
packageName: string,
|
||||
packageVersion: string
|
||||
) => {
|
||||
return getNavUrlParams(
|
||||
{
|
||||
'vulnerability.id': vulnerabilityId,
|
||||
'resource.id': resourceId,
|
||||
'package.name': encodeURIComponent(packageName),
|
||||
'package.version': encodeURIComponent(packageVersion),
|
||||
const sorting: EuiTableSortingType<VulnerabilitiesFindingDetailFields> = {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
'vulnerabilities'
|
||||
};
|
||||
|
||||
const { data } = useVulnerabilitiesFindings({
|
||||
query: buildVulnerabilityEntityFlyoutPreviewQuery('host.name', value, currentFilter),
|
||||
sort: [sortFieldDirection],
|
||||
enabled: true,
|
||||
pageSize: 1,
|
||||
});
|
||||
|
||||
const { counts } = useHasVulnerabilities('host.name', value);
|
||||
|
||||
const { critical = 0, high = 0, medium = 0, low = 0, none = 0 } = counts || {};
|
||||
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const findingsPagination = (findings: VulnerabilitiesFindingDetailFields[]) => {
|
||||
let pageOfItems;
|
||||
|
||||
if (!pageIndex && !pageSize) {
|
||||
pageOfItems = findings;
|
||||
} else {
|
||||
const startIndex = pageIndex * pageSize;
|
||||
pageOfItems = findings?.slice(
|
||||
startIndex,
|
||||
Math.min(startIndex + pageSize, findings?.length)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
pageOfItems,
|
||||
totalItemCount: findings?.length,
|
||||
};
|
||||
};
|
||||
|
||||
const { pageOfItems, totalItemCount } = findingsPagination(data?.rows || []);
|
||||
|
||||
const pagination = {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount,
|
||||
pageSizeOptions: [10, 25, 100],
|
||||
};
|
||||
|
||||
const onTableChange = ({ page, sort }: Criteria<VulnerabilitiesFindingDetailFields>) => {
|
||||
if (page) {
|
||||
const { index, size } = page;
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
}
|
||||
if (sort) {
|
||||
const { field: fieldSort, direction } = sort;
|
||||
setSortField(fieldSort);
|
||||
setSortDirection(direction);
|
||||
}
|
||||
};
|
||||
|
||||
const getNavUrlParams: ReturnType<typeof useGetNavigationUrlParams> =
|
||||
useGetNavigationUrlParams();
|
||||
|
||||
const getVulnerabilityUrl = (name: string, queryField: CloudPostureEntityIdentifier) => {
|
||||
return getNavUrlParams({ [queryField]: name }, 'vulnerabilities');
|
||||
};
|
||||
|
||||
const vulnerabilityStats = getVulnerabilityStats(
|
||||
{
|
||||
critical,
|
||||
high,
|
||||
medium,
|
||||
low,
|
||||
none,
|
||||
},
|
||||
getSeverityStatusColor,
|
||||
setCurrentFilter,
|
||||
currentFilter
|
||||
);
|
||||
};
|
||||
|
||||
const vulnerabilityStats = getVulnerabilityStats(
|
||||
{
|
||||
critical,
|
||||
high,
|
||||
medium,
|
||||
low,
|
||||
none,
|
||||
},
|
||||
getSeverityStatusColor,
|
||||
setCurrentFilter,
|
||||
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 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 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',
|
||||
}
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
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} />;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
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',
|
||||
name: '',
|
||||
width: '5%',
|
||||
render: (
|
||||
vulnerability: VulnerabilitiesPackage,
|
||||
finding: VulnerabilitiesFindingDetailFields
|
||||
) => (
|
||||
<EuiButtonIcon
|
||||
iconType="expand"
|
||||
onClick={() => {
|
||||
const previewPanelProps: FindingsVulnerabilityPanelExpandableFlyoutPropsPreview = {
|
||||
id: VulnerabilityFindingsPreviewPanelKey,
|
||||
params: {
|
||||
vulnerabilityId: vulnerability?.id,
|
||||
resourceId: finding?.resource?.id || '',
|
||||
packageName: vulnerability?.package?.name,
|
||||
packageVersion: vulnerability?.package?.version,
|
||||
eventId: finding?.event?.id || '',
|
||||
scopeId,
|
||||
isPreviewMode: true,
|
||||
banner: {
|
||||
title: i18n.translate(
|
||||
'xpack.securitySolution.flyout.right.vulnerabilityFinding.PreviewTitle',
|
||||
{
|
||||
defaultMessage: 'Preview vulnerability details',
|
||||
}
|
||||
),
|
||||
backgroundColor: 'warning',
|
||||
textColor: 'warning',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
openPreviewPanel(previewPanelProps);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
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) => {
|
||||
if (Array.isArray(title)) {
|
||||
return <EuiText size="s">{title.join(', ')}</EuiText>;
|
||||
}
|
||||
|
||||
return <EuiText size="s">{title || EMPTY_VALUE}</EuiText>;
|
||||
},
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.vulnerability.table.vulnerabilityTitleColumnName',
|
||||
{ defaultMessage: 'Vulnerability Title' }
|
||||
),
|
||||
width: '25%',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: VULNERABILITY.ID,
|
||||
render: (id: string, finding: VulnerabilitiesFindingDetailFields) =>
|
||||
renderMultiValueCell(VULNERABILITY.ID, finding),
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.vulnerability.table.vulnerabilityIdColumnName',
|
||||
{ defaultMessage: 'CVE ID' }
|
||||
),
|
||||
width: '20%',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: VULNERABILITY.SEVERITY,
|
||||
render: (severity: string) => (
|
||||
<>
|
||||
<EuiText size="s">
|
||||
<SeverityStatusBadge severity={getNormalizedSeverity(severity)} />
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.vulnerability.table.ruleColumnName',
|
||||
{ defaultMessage: 'Severity' }
|
||||
),
|
||||
width: '10%',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: VULNERABILITY.PACKAGE_NAME,
|
||||
render: (packageName: string, finding: VulnerabilitiesFindingDetailFields) =>
|
||||
renderMultiValueCell(VULNERABILITY.PACKAGE_NAME, finding),
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.vulnerability.table.ruleColumnName',
|
||||
{ defaultMessage: 'Package' }
|
||||
),
|
||||
width: '30%',
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<MultiValueCellPopover<VulnerabilitiesFindingDetailFields>
|
||||
items={cellValue}
|
||||
field={field}
|
||||
object={finding}
|
||||
renderItem={renderItem}
|
||||
firstItemRenderer={(item) => <EuiText size="s">{item}</EuiText>}
|
||||
/>
|
||||
<>
|
||||
<EuiPanel hasShadow={false}>
|
||||
<SecuritySolutionLinkAnchor
|
||||
deepLinkId={SecurityPageName.cloudSecurityPostureFindings}
|
||||
path={`${getVulnerabilityUrl(value, EntityIdentifierFields.hostName)}`}
|
||||
target={'_blank'}
|
||||
external={false}
|
||||
onClick={() => {
|
||||
uiMetricService.trackUiMetric(
|
||||
METRIC_TYPE.CLICK,
|
||||
NAV_TO_FINDINGS_BY_HOST_NAME_FROM_ENTITY_FLYOUT
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.vulnerability.tableTitle',
|
||||
{
|
||||
defaultMessage: 'Vulnerability ',
|
||||
}
|
||||
)}
|
||||
<EuiIcon type={'popout'} />
|
||||
</SecuritySolutionLinkAnchor>
|
||||
<EuiSpacer size="xl" />
|
||||
<DistributionBar stats={vulnerabilityStats} />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiBasicTable
|
||||
items={pageOfItems || []}
|
||||
rowHeader="result"
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
onChange={onTableChange}
|
||||
data-test-subj={'securitySolutionFlyoutVulnerabilitiesFindingsTable'}
|
||||
sorting={sorting}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<VulnerabilitiesFindingDetailFields>> = [
|
||||
{
|
||||
field: 'vulnerability',
|
||||
name: '',
|
||||
width: '5%',
|
||||
render: (
|
||||
vulnerability: VulnerabilitiesPackage,
|
||||
finding: VulnerabilitiesFindingDetailFields
|
||||
) => (
|
||||
<SecuritySolutionLinkAnchor
|
||||
deepLinkId={SecurityPageName.cloudSecurityPostureFindings}
|
||||
path={`${getVulnerabilityUrlFilteredByVulnerabilityAndResourceId(
|
||||
vulnerability?.id,
|
||||
finding?.resource?.id || '',
|
||||
vulnerability?.package?.name,
|
||||
vulnerability?.package?.version
|
||||
)}`}
|
||||
target={'_blank'}
|
||||
external={false}
|
||||
>
|
||||
<EuiIcon type={'popout'} />
|
||||
</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) => {
|
||||
if (Array.isArray(title)) {
|
||||
return <EuiText size="s">{title.join(', ')}</EuiText>;
|
||||
}
|
||||
|
||||
return <EuiText size="s">{title || EMPTY_VALUE}</EuiText>;
|
||||
},
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.vulnerability.table.vulnerabilityTitleColumnName',
|
||||
{ defaultMessage: 'Vulnerability Title' }
|
||||
),
|
||||
width: '25%',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: VULNERABILITY.ID,
|
||||
render: (id: string, finding: VulnerabilitiesFindingDetailFields) =>
|
||||
renderMultiValueCell(VULNERABILITY.ID, finding),
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.vulnerability.table.vulnerabilityIdColumnName',
|
||||
{ defaultMessage: 'CVE ID' }
|
||||
),
|
||||
width: '20%',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: VULNERABILITY.SEVERITY,
|
||||
render: (severity: string) => (
|
||||
<>
|
||||
<EuiText size="s">
|
||||
<SeverityStatusBadge severity={getNormalizedSeverity(severity)} />
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.vulnerability.table.ruleColumnName',
|
||||
{ defaultMessage: 'Severity' }
|
||||
),
|
||||
width: '10%',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: VULNERABILITY.PACKAGE_NAME,
|
||||
render: (packageName: string, finding: VulnerabilitiesFindingDetailFields) =>
|
||||
renderMultiValueCell(VULNERABILITY.PACKAGE_NAME, finding),
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.flyout.left.insights.vulnerability.table.ruleColumnName',
|
||||
{ defaultMessage: 'Package' }
|
||||
),
|
||||
width: '30%',
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPanel hasShadow={false}>
|
||||
<SecuritySolutionLinkAnchor
|
||||
deepLinkId={SecurityPageName.cloudSecurityPostureFindings}
|
||||
path={`${getVulnerabilityUrl(value, EntityIdentifierFields.hostName)}`}
|
||||
target={'_blank'}
|
||||
external={false}
|
||||
onClick={() => {
|
||||
uiMetricService.trackUiMetric(
|
||||
METRIC_TYPE.CLICK,
|
||||
NAV_TO_FINDINGS_BY_HOST_NAME_FROM_ENTITY_FLYOUT
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.securitySolution.flyout.left.insights.vulnerability.tableTitle', {
|
||||
defaultMessage: 'Vulnerability ',
|
||||
})}
|
||||
<EuiIcon type={'popout'} />
|
||||
</SecuritySolutionLinkAnchor>
|
||||
<EuiSpacer size="xl" />
|
||||
<DistributionBar stats={vulnerabilityStats} />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiBasicTable
|
||||
items={pageOfItems || []}
|
||||
rowHeader="result"
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
onChange={onTableChange}
|
||||
data-test-subj={'securitySolutionFlyoutVulnerabilitiesFindingsTable'}
|
||||
sorting={sorting}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
VulnerabilitiesFindingsDetailsTable.displayName = 'VulnerabilitiesFindingsDetailsTable';
|
||||
|
|
|
@ -10,20 +10,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiFlyoutFooter } from '
|
|||
import type { FindingsMisconfigurationPanelExpandableFlyoutProps } from '@kbn/cloud-security-posture';
|
||||
import { CspEvaluationBadge } from '@kbn/cloud-security-posture';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useGetNavigationUrlParams } from '@kbn/cloud-security-posture/src/hooks/use_get_navigation_url_params';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import {
|
||||
uiMetricService,
|
||||
NAV_TO_FINDINGS_BY_RULE_NAME_FROM_ENTITY_FLYOUT,
|
||||
} from '@kbn/cloud-security-posture-common/utils/ui_metrics';
|
||||
import { SecurityPageName } from '@kbn/deeplinks-security';
|
||||
import { FlyoutNavigation } from '../../../shared/components/flyout_navigation';
|
||||
import { FlyoutHeader } from '../../../shared/components/flyout_header';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { PreferenceFormattedDate } from '../../../../common/components/formatted_date';
|
||||
import { FlyoutTitle } from '../../../shared/components/flyout_title';
|
||||
import { FlyoutBody } from '../../../shared/components/flyout_body';
|
||||
import { SecuritySolutionLinkAnchor } from '../../../../common/components/links';
|
||||
|
||||
export const FindingsMisconfigurationPanel = ({
|
||||
resourceId,
|
||||
|
@ -33,20 +25,9 @@ export const FindingsMisconfigurationPanel = ({
|
|||
const { cloudSecurityPosture } = useKibana().services;
|
||||
const CspFlyout = cloudSecurityPosture.getCloudSecurityPostureMisconfigurationFlyout();
|
||||
|
||||
const getNavUrlParams = useGetNavigationUrlParams();
|
||||
const getFindingsPageUrlFilteredByRuleAndResourceId = (
|
||||
findingRuleId: string,
|
||||
findingResourceId: string
|
||||
) => {
|
||||
return getNavUrlParams(
|
||||
{ 'rule.id': findingRuleId, 'resource.id': findingResourceId },
|
||||
'configurations'
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FlyoutNavigation flyoutIsExpandable={false} />
|
||||
<FlyoutNavigation flyoutIsExpandable={false} isPreviewMode={isPreviewMode} />
|
||||
<CspFlyout.Component ruleId={ruleId} resourceId={resourceId}>
|
||||
{({ finding, createRuleFn }) => {
|
||||
return (
|
||||
|
@ -70,27 +51,7 @@ export const FindingsMisconfigurationPanel = ({
|
|||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
{isPreviewMode ? (
|
||||
<SecuritySolutionLinkAnchor
|
||||
deepLinkId={SecurityPageName.cloudSecurityPostureFindings}
|
||||
path={`${getFindingsPageUrlFilteredByRuleAndResourceId(
|
||||
finding.rule.id,
|
||||
finding.resource.id
|
||||
)}`}
|
||||
target={'_blank'}
|
||||
external={false}
|
||||
onClick={() => {
|
||||
uiMetricService.trackUiMetric(
|
||||
METRIC_TYPE.CLICK,
|
||||
NAV_TO_FINDINGS_BY_RULE_NAME_FROM_ENTITY_FLYOUT
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FlyoutTitle title={finding.rule.name} isLink />
|
||||
</SecuritySolutionLinkAnchor>
|
||||
) : (
|
||||
<FlyoutTitle title={finding.rule.name} />
|
||||
)}
|
||||
<FlyoutTitle title={finding.rule.name} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<CspFlyout.Header finding={finding} />
|
||||
|
|
|
@ -5,7 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { FindingVulnerabilityPanelExpandableFlyoutProps } from '@kbn/cloud-security-posture';
|
||||
import type {
|
||||
FindingsVulnerabilityPanelExpandableFlyoutPropsNonPreview,
|
||||
FindingsVulnerabilityPanelExpandableFlyoutPropsPreview,
|
||||
} from '@kbn/cloud-security-posture';
|
||||
|
||||
export const VulnerabilityFindingsPanelKey: FindingVulnerabilityPanelExpandableFlyoutProps['key'] =
|
||||
export const VulnerabilityFindingsPanelKey: FindingsVulnerabilityPanelExpandableFlyoutPropsNonPreview['id'] =
|
||||
'findings-vulnerability-panel';
|
||||
|
||||
export const VulnerabilityFindingsPreviewPanelKey: FindingsVulnerabilityPanelExpandableFlyoutPropsPreview['id'] =
|
||||
'findings-vulnerability-panel-preview';
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 { render, screen } from '@testing-library/react';
|
||||
import { FindingsVulnerabilityPanel } from '.';
|
||||
import { useDateFormat, useKibana, useTimeZone } from '../../../../common/lib/kibana';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import {
|
||||
useExpandableFlyoutApi,
|
||||
useExpandableFlyoutHistory,
|
||||
useExpandableFlyoutState,
|
||||
} from '@kbn/expandable-flyout';
|
||||
import type { FindingsVulnerabilityPanelExpandableFlyoutProps } from '@kbn/cloud-security-posture';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana', () => ({
|
||||
useKibana: jest.fn(),
|
||||
useDateFormat: jest.fn(),
|
||||
useTimeZone: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/expandable-flyout', () => ({
|
||||
useExpandableFlyoutApi: jest.fn(),
|
||||
useExpandableFlyoutHistory: jest.fn(),
|
||||
useExpandableFlyoutState: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/cloud-security-posture/src/hooks/use_get_navigation_url_params', () => ({
|
||||
useGetNavigationUrlParams: () => () => 'mocked-nav-url',
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const renderComponent = (Component: any) => {
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
cloudSecurityPosture: {
|
||||
getCloudSecurityPostureVulnerabilityFlyout: () => ({
|
||||
Component,
|
||||
Header: () => <div />,
|
||||
Body: () => <div />,
|
||||
Footer: () => <div />,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<TestProviders>
|
||||
<FindingsVulnerabilityPanel {...baseProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
};
|
||||
|
||||
const createMockFlyoutComponent =
|
||||
(title?: string) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
({ children }: any) =>
|
||||
children({
|
||||
finding: {
|
||||
vulnerability: {
|
||||
title,
|
||||
severity: 'High',
|
||||
published_date: '2025-05-22T00:00:00.000Z',
|
||||
},
|
||||
'@timestamp': '2025-05-22T00:00:00.000Z',
|
||||
},
|
||||
createRuleFn: jest.fn(),
|
||||
});
|
||||
|
||||
const baseProps: FindingsVulnerabilityPanelExpandableFlyoutProps['params'] = {
|
||||
vulnerabilityId: 'vulnerability_id',
|
||||
resourceId: 'resource_id',
|
||||
packageName: 'package_name',
|
||||
packageVersion: 'package_version',
|
||||
eventId: 'event_id',
|
||||
isPreviewMode: false,
|
||||
};
|
||||
|
||||
const flyoutHistoryMock = [{ lastOpen: Date.now(), panel: { id: 'id_mock', params: {} } }];
|
||||
|
||||
beforeEach(() => {
|
||||
(useExpandableFlyoutApi as jest.Mock).mockReturnValue({ closeLeftPanel: jest.fn() });
|
||||
(useExpandableFlyoutHistory as jest.Mock).mockReturnValue(flyoutHistoryMock);
|
||||
(useExpandableFlyoutState as jest.Mock).mockReturnValue({});
|
||||
|
||||
(useDateFormat as jest.Mock).mockReturnValue('MMM D, YYYY @ HH:mm:ss.SSS');
|
||||
(useTimeZone as jest.Mock).mockReturnValue('UTC');
|
||||
});
|
||||
|
||||
describe('FindingsVulnerabilityPanel', () => {
|
||||
it('renders the vulnerability title when available', () => {
|
||||
renderComponent(createMockFlyoutComponent('Test Vulnerability Title'));
|
||||
expect(screen.getByText('Test Vulnerability Title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders fallback title when vulnerability title is missing', () => {
|
||||
renderComponent(createMockFlyoutComponent(undefined));
|
||||
expect(screen.getByText('No title available')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -6,10 +6,10 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { FindingsVulnerabilityPanelExpandableFlyoutProps } from '@kbn/cloud-security-posture';
|
||||
import {
|
||||
SeverityStatusBadge,
|
||||
getNormalizedSeverity,
|
||||
type FindingVulnerabilityFlyoutProps,
|
||||
type FindingVulnerabilityFullFlyoutContentProps,
|
||||
} from '@kbn/cloud-security-posture';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFlyoutFooter, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
|
@ -20,6 +20,7 @@ import { FlyoutNavigation } from '../../../shared/components/flyout_navigation';
|
|||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { FlyoutHeader } from '../../../shared/components/flyout_header';
|
||||
import { FlyoutBody } from '../../../shared/components/flyout_body';
|
||||
import { FlyoutTitle } from '../../../shared/components/flyout_title';
|
||||
|
||||
export const FindingsVulnerabilityPanel = ({
|
||||
vulnerabilityId,
|
||||
|
@ -27,13 +28,14 @@ export const FindingsVulnerabilityPanel = ({
|
|||
packageName,
|
||||
packageVersion,
|
||||
eventId,
|
||||
}: FindingVulnerabilityFlyoutProps) => {
|
||||
isPreviewMode,
|
||||
}: FindingsVulnerabilityPanelExpandableFlyoutProps['params']) => {
|
||||
const { cloudSecurityPosture } = useKibana().services;
|
||||
const CspVulnerabilityFlyout = cloudSecurityPosture.getCloudSecurityPostureVulnerabilityFlyout();
|
||||
|
||||
return (
|
||||
<>
|
||||
<FlyoutNavigation flyoutIsExpandable={false} />
|
||||
<FlyoutNavigation flyoutIsExpandable={false} isPreviewMode={isPreviewMode} />
|
||||
<CspVulnerabilityFlyout.Component
|
||||
vulnerabilityId={vulnerabilityId}
|
||||
resourceId={resourceId}
|
||||
|
@ -42,6 +44,12 @@ export const FindingsVulnerabilityPanel = ({
|
|||
eventId={eventId}
|
||||
>
|
||||
{({ finding, createRuleFn }: FindingVulnerabilityFullFlyoutContentProps) => {
|
||||
const vulnerabilityTitle =
|
||||
finding?.vulnerability?.title ??
|
||||
i18n.translate('xpack.securitySolution.csp.vulnerabilitiesFlyout.emptyTitleHolder', {
|
||||
defaultMessage: 'No title available',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<FlyoutHeader>
|
||||
|
@ -79,14 +87,18 @@ export const FindingsVulnerabilityPanel = ({
|
|||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<FlyoutTitle title={vulnerabilityTitle} />
|
||||
<EuiSpacer size="xs" />
|
||||
<CspVulnerabilityFlyout.Header finding={finding} />
|
||||
</FlyoutHeader>
|
||||
<FlyoutBody>
|
||||
<CspVulnerabilityFlyout.Body finding={finding} />
|
||||
</FlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<CspVulnerabilityFlyout.Footer createRuleFn={createRuleFn} />
|
||||
</EuiFlyoutFooter>
|
||||
{!isPreviewMode && (
|
||||
<EuiFlyoutFooter>
|
||||
<CspVulnerabilityFlyout.Footer createRuleFn={createRuleFn} />
|
||||
</EuiFlyoutFooter>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -9,9 +9,10 @@ import React, { memo, useCallback } from 'react';
|
|||
import { ExpandableFlyout, type ExpandableFlyoutProps } from '@kbn/expandable-flyout';
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import type {
|
||||
FindingVulnerabilityPanelExpandableFlyoutProps,
|
||||
FindingsMisconfigurationPanelExpandableFlyoutPropsNonPreview,
|
||||
FindingsMisconfigurationPanelExpandableFlyoutPropsPreview,
|
||||
FindingsVulnerabilityPanelExpandableFlyoutPropsNonPreview,
|
||||
FindingsVulnerabilityPanelExpandableFlyoutPropsPreview,
|
||||
} from '@kbn/cloud-security-posture';
|
||||
import type { GenericEntityDetailsExpandableFlyoutProps } from './entity_details/generic_details_left';
|
||||
import {
|
||||
|
@ -77,7 +78,10 @@ import {
|
|||
} from './csp_details/findings_flyout/constants';
|
||||
import { FindingsMisconfigurationPanel } from './csp_details/findings_flyout/findings_right';
|
||||
import { IOCPanelKey } from './ai_for_soc/constants/panel_keys';
|
||||
import { VulnerabilityFindingsPanelKey } from './csp_details/vulnerabilities_flyout/constants';
|
||||
import {
|
||||
VulnerabilityFindingsPanelKey,
|
||||
VulnerabilityFindingsPreviewPanelKey,
|
||||
} from './csp_details/vulnerabilities_flyout/constants';
|
||||
import { FindingsVulnerabilityPanel } from './csp_details/vulnerabilities_flyout/vulnerabilities_right';
|
||||
|
||||
/**
|
||||
|
@ -242,7 +246,15 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels']
|
|||
key: VulnerabilityFindingsPanelKey,
|
||||
component: (props) => (
|
||||
<FindingsVulnerabilityPanel
|
||||
{...(props as FindingVulnerabilityPanelExpandableFlyoutProps).params}
|
||||
{...(props as FindingsVulnerabilityPanelExpandableFlyoutPropsNonPreview).params}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: VulnerabilityFindingsPreviewPanelKey,
|
||||
component: (props) => (
|
||||
<FindingsVulnerabilityPanel
|
||||
{...(props as FindingsVulnerabilityPanelExpandableFlyoutPropsPreview).params}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue