[Cloud Posture] Tables shared values (#140295)

This commit is contained in:
Jordan 2022-09-20 18:13:58 +03:00 committed by GitHub
parent b5a35d74e7
commit 49b39dff2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 249 additions and 61 deletions

View file

@ -6,8 +6,44 @@
*/
import { euiPaletteForStatus } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { CLOUDBEAT_EKS, CLOUDBEAT_VANILLA } from '../../common/constants';
const [success, warning, danger] = euiPaletteForStatus(3);
export const statusColors = { success, warning, danger };
export const CSP_MOMENT_FORMAT = 'MMMM D, YYYY @ HH:mm:ss.SSS';
export type CloudPostureIntegrations = typeof cloudPostureIntegrations;
export const cloudPostureIntegrations = {
kspm: {
policyTemplate: 'kspm',
name: i18n.translate('xpack.csp.kspmIntegration.integration.nameTitle', {
defaultMessage: 'Kubernetes Security Posture Management',
}),
shortName: i18n.translate('xpack.csp.kspmIntegration.integration.shortNameTitle', {
defaultMessage: 'KSPM',
}),
options: [
{
type: CLOUDBEAT_VANILLA,
name: i18n.translate('xpack.csp.kspmIntegration.vanillaOption.nameTitle', {
defaultMessage: 'Unmanaged Kubernetes',
}),
benchmark: i18n.translate('xpack.csp.kspmIntegration.vanillaOption.benchmarkTitle', {
defaultMessage: 'CIS Kubernetes',
}),
},
{
type: CLOUDBEAT_EKS,
name: i18n.translate('xpack.csp.kspmIntegration.eksOption.nameTitle', {
defaultMessage: 'EKS (Elastic Kubernetes Service)',
}),
benchmark: i18n.translate('xpack.csp.kspmIntegration.eksOption.benchmarkTitle', {
defaultMessage: 'CIS EKS',
}),
},
],
},
} as const;

View file

@ -0,0 +1,48 @@
/*
* 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 { EuiDescriptionList, useEuiTheme, type EuiDescriptionListProps } from '@elastic/eui';
const getModifiedTitlesListItems = (listItems: EuiDescriptionListProps['listItems']) =>
listItems
?.filter((item) => !!item?.title && !!item?.description)
.map((item) => ({ ...item, title: `${item.title}:` }));
// eui size m is 12px which is too small, and next after it is base which is 16px which is too big
const fontSize = '1rem';
export const CspInlineDescriptionList = ({
listItems,
}: {
listItems: EuiDescriptionListProps['listItems'];
}) => {
const { euiTheme } = useEuiTheme();
const modifiedTitlesListItems = getModifiedTitlesListItems(listItems);
return (
<EuiDescriptionList
type="inline"
titleProps={{
style: {
background: 'initial',
color: euiTheme.colors.subduedText,
fontSize,
paddingRight: 0,
},
}}
descriptionProps={{
style: {
color: euiTheme.colors.subduedText,
marginRight: euiTheme.size.xs,
fontSize,
},
}}
listItems={modifiedTitlesListItems}
/>
);
};

View file

@ -16,7 +16,7 @@ import {
EuiDescribedFormGroup,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { cloudPostureIntegrations } from '../../common/constants';
import { CLOUDBEAT_EKS, CLOUDBEAT_VANILLA } from '../../../common/constants';
export type InputType = typeof CLOUDBEAT_EKS | typeof CLOUDBEAT_VANILLA;
@ -27,22 +27,8 @@ interface Props {
isDisabled?: boolean;
}
const kubeDeployOptions: Array<EuiComboBoxOptionOption<InputType>> = [
{
value: CLOUDBEAT_VANILLA,
label: i18n.translate(
'xpack.csp.createPackagePolicy.stepConfigure.integrationSettingsSection.vanillaKubernetesDeploymentOption',
{ defaultMessage: 'Unmanaged Kubernetes' }
),
},
{
value: CLOUDBEAT_EKS,
label: i18n.translate(
'xpack.csp.createPackagePolicy.stepConfigure.integrationSettingsSection.eksKubernetesDeploymentOption',
{ defaultMessage: 'EKS (Elastic Kubernetes Service)' }
),
},
];
const kubeDeployOptions: Array<EuiComboBoxOptionOption<InputType>> =
cloudPostureIntegrations.kspm.options.map((o) => ({ value: o.type, label: o.name }));
const KubernetesDeploymentFieldLabel = () => (
<EuiToolTip

View file

@ -18,8 +18,8 @@ import { CspFinding } from '../../../../common/schemas/csp_finding';
const chance = new Chance();
const getFakeFindings = (name: string): CspFinding & { id: string } => ({
id: chance.word(),
cluster_id: chance.guid(),
id: chance.word(),
result: {
expected: {
source: {},

View file

@ -5,11 +5,17 @@
* 2.0.
*/
import React from 'react';
import { EuiSpacer, EuiButtonEmpty } from '@elastic/eui';
import {
EuiSpacer,
EuiButtonEmpty,
EuiPageHeader,
type EuiDescriptionListProps,
} from '@elastic/eui';
import { Link, useParams } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n-react';
import { generatePath } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { CspInlineDescriptionList } from '../../../../components/csp_inline_description_list';
import type { Evaluation } from '../../../../../common/types';
import { CspFinding } from '../../../../../common/schemas/csp_finding';
import { CloudPosturePageTitle } from '../../../../components/cloud_posture_page_title';
@ -54,6 +60,32 @@ const BackToResourcesButton = () => (
</Link>
);
const getResourceFindingSharedValues = (sharedValues: {
resourceId: string;
resourceSubType: string;
resourceName: string;
clusterId: string;
}): EuiDescriptionListProps['listItems'] => [
{
title: i18n.translate('xpack.csp.findings.resourceFindingsSharedValues.resourceTypeTitle', {
defaultMessage: 'Resource Type',
}),
description: sharedValues.resourceSubType,
},
{
title: i18n.translate('xpack.csp.findings.resourceFindingsSharedValues.resourceIdTitle', {
defaultMessage: 'Resource ID',
}),
description: sharedValues.resourceId,
},
{
title: i18n.translate('xpack.csp.findings.resourceFindingsSharedValues.clusterIdTitle', {
defaultMessage: 'Cluster ID',
}),
description: sharedValues.clusterId,
},
];
export const ResourceFindings = ({ dataView }: FindingsBaseProps) => {
const params = useParams<{ resourceId: string }>();
const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery);
@ -114,14 +146,28 @@ export const ResourceFindings = ({ dataView }: FindingsBaseProps) => {
title={i18n.translate(
'xpack.csp.findings.resourceFindings.resourceFindingsPageTitle',
{
defaultMessage: '{resourceId} - Findings',
values: { resourceId: params.resourceId },
defaultMessage: '{resourceName} - Findings',
values: { resourceName: resourceFindings.data?.resourceName },
}
)}
/>
}
/>
</PageTitle>
<EuiPageHeader
description={
resourceFindings.data && (
<CspInlineDescriptionList
listItems={getResourceFindingSharedValues({
resourceId: params.resourceId,
resourceName: resourceFindings.data.resourceName,
resourceSubType: resourceFindings.data.resourceSubType,
clusterId: resourceFindings.data.clusterId,
})}
/>
)
}
/>
<EuiSpacer />
{error && <ErrorCallout error={error} />}
{!error && (

View file

@ -49,21 +49,11 @@ const ResourceFindingsTableComponent = ({
] = useMemo(
() => [
getExpandColumn<CspFinding>({ onClick: setSelectedFinding }),
baseFindingsColumns['resource.id'],
createColumnWithFilters(baseFindingsColumns['result.evaluation'], { onAddFilter }),
createColumnWithFilters(
{ ...baseFindingsColumns['resource.sub_type'], sortable: false },
{ onAddFilter }
),
createColumnWithFilters(
{ ...baseFindingsColumns['resource.name'], sortable: false },
{ onAddFilter }
),
createColumnWithFilters(baseFindingsColumns['rule.name'], { onAddFilter }),
createColumnWithFilters(baseFindingsColumns['rule.benchmark.name'], { onAddFilter }),
baseFindingsColumns['rule.section'],
baseFindingsColumns['rule.tags'],
createColumnWithFilters(baseFindingsColumns.cluster_id, { onAddFilter }),
baseFindingsColumns['@timestamp'],
],
[onAddFilter]

View file

@ -34,11 +34,14 @@ export interface ResourceFindingsQuery {
}
type ResourceFindingsRequest = IKibanaSearchRequest<estypes.SearchRequest>;
type ResourceFindingsResponse = IKibanaSearchResponse<estypes.SearchResponse<CspFinding, Aggs>>;
type ResourceFindingsResponse = IKibanaSearchResponse<
estypes.SearchResponse<CspFinding, ResourceFindingsResponseAggs>
>;
interface Aggs {
count: estypes.AggregationsMultiBucketAggregateBase<estypes.AggregationsStringRareTermsBucketKeys>;
}
export type ResourceFindingsResponseAggs = Record<
'count' | 'clusterId' | 'resourceSubType' | 'resourceName',
estypes.AggregationsMultiBucketAggregateBase<estypes.AggregationsStringRareTermsBucketKeys>
>;
const getResourceFindingsQuery = ({
query,
@ -60,7 +63,18 @@ const getResourceFindingsQuery = ({
},
sort: [{ [sort.field]: sort.direction }],
pit: { id: pitId },
aggs: getFindingsCountAggQuery(),
aggs: {
...getFindingsCountAggQuery(),
clusterId: {
terms: { field: 'cluster_id' },
},
resourceSubType: {
terms: { field: 'resource.sub_type' },
},
resourceName: {
terms: { field: 'resource.name' },
},
},
},
ignore_unavailable: false,
});
@ -90,13 +104,18 @@ export const useResourceFindings = (options: UseResourceFindingsOptions) => {
}: ResourceFindingsResponse) => {
if (!aggregations) throw new Error('expected aggregations to exists');
if (!Array.isArray(aggregations?.count.buckets))
throw new Error('expected buckets to be an array');
assertNonEmptyArray(aggregations.count.buckets);
assertNonEmptyArray(aggregations.clusterId.buckets);
assertNonEmptyArray(aggregations.resourceSubType.buckets);
assertNonEmptyArray(aggregations.resourceName.buckets);
return {
page: hits.hits.map((hit) => hit._source!),
total: number.is(hits.total) ? hits.total : 0,
count: getAggregationCount(aggregations.count.buckets),
clusterId: getFirstBucketKey(aggregations.clusterId.buckets),
resourceSubType: getFirstBucketKey(aggregations.resourceSubType.buckets),
resourceName: getFirstBucketKey(aggregations.resourceName.buckets),
newPitId: newPitId!,
};
},
@ -110,3 +129,12 @@ export const useResourceFindings = (options: UseResourceFindingsOptions) => {
}
);
};
function assertNonEmptyArray<T>(arr: unknown): asserts arr is T[] {
if (!Array.isArray(arr) || arr.length === 0) {
throw new Error('expected a non empty array');
}
}
const getFirstBucketKey = (buckets: estypes.AggregationsStringRareTermsBucketKeys[]) =>
buckets[0].key;

View file

@ -7,10 +7,18 @@
import React, { useContext, useMemo } from 'react';
import { generatePath, Link, type RouteComponentProps } from 'react-router-dom';
import { EuiTextColor, EuiButtonEmpty, EuiFlexGroup, EuiPageHeader, EuiSpacer } from '@elastic/eui';
import {
EuiButtonEmpty,
type EuiDescriptionListProps,
EuiFlexGroup,
EuiPageHeader,
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { pagePathGetters } from '@kbn/fleet-plugin/public';
import { i18n } from '@kbn/i18n';
import { PackagePolicy } from '@kbn/fleet-plugin/common';
import { CspInlineDescriptionList } from '../../components/csp_inline_description_list';
import { CloudPosturePageTitle } from '../../components/cloud_posture_page_title';
import type { BreadcrumbEntry } from '../../common/navigation/types';
import { RulesContainer, type PageUrlParams } from './rules_container';
@ -20,6 +28,8 @@ import { useCspIntegrationInfo } from './use_csp_integration';
import { useKibana } from '../../common/hooks/use_kibana';
import { CloudPosturePage } from '../../components/cloud_posture_page';
import { SecuritySolutionContext } from '../../application/security_solution_context';
import { CloudPostureIntegrations, cloudPostureIntegrations } from '../../common/constants';
import * as TEST_SUBJECTS from './test_subjects';
const getRulesBreadcrumbs = (
name?: string,
@ -41,12 +51,55 @@ const getRulesBreadcrumbs = (
return breadCrumbs;
};
const isPolicyTemplate = (name: unknown): name is keyof CloudPostureIntegrations =>
typeof name === 'string' && name in cloudPostureIntegrations;
const getRulesSharedValues = (
packageInfo?: PackagePolicy
): NonNullable<EuiDescriptionListProps['listItems']> => {
const enabledInput = packageInfo?.inputs.find((input) => input.enabled);
if (!enabledInput || !isPolicyTemplate(enabledInput.policy_template)) return [];
const integration = cloudPostureIntegrations[enabledInput.policy_template];
const enabledIntegrationOption = integration.options.find(
(option) => option.type === enabledInput.type
);
const values = [
{
title: i18n.translate('xpack.csp.rules.rulesPageSharedValues.integrationTitle', {
defaultMessage: 'Integration',
}),
description: integration.shortName,
},
];
if (!enabledIntegrationOption) return values;
values.push(
{
title: i18n.translate('xpack.csp.rules.rulesPageSharedValues.deploymentTypeTitle', {
defaultMessage: 'Deployment Type',
}),
description: enabledIntegrationOption.name,
},
{
title: i18n.translate('xpack.csp.rules.rulesPageSharedValues.benchmarkTitle', {
defaultMessage: 'Benchmark',
}),
description: enabledIntegrationOption.benchmark,
}
);
return values;
};
export const Rules = ({ match: { params } }: RouteComponentProps<PageUrlParams>) => {
const { http } = useKibana().services;
const integrationInfo = useCspIntegrationInfo(params);
const securitySolutionContext = useContext(SecuritySolutionContext);
const [packageInfo, agentInfo] = integrationInfo.data || [];
const [packageInfo] = integrationInfo.data || [];
const breadcrumbs = useMemo(
() =>
@ -56,6 +109,8 @@ export const Rules = ({ match: { params } }: RouteComponentProps<PageUrlParams>)
useCspBreadcrumbs(breadcrumbs);
const sharedValues = getRulesSharedValues(packageInfo);
return (
<CloudPosturePage query={integrationInfo}>
<EuiPageHeader
@ -93,18 +148,10 @@ export const Rules = ({ match: { params } }: RouteComponentProps<PageUrlParams>)
</EuiFlexGroup>
}
description={
packageInfo?.package &&
agentInfo?.name && (
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.csp.rules.rulePageHeader.pageDescriptionTitle"
defaultMessage="{integrationType}, {agentPolicyName}"
values={{
integrationType: packageInfo.package.title,
agentPolicyName: agentInfo.name,
}}
/>
</EuiTextColor>
sharedValues.length && (
<div data-test-subj={TEST_SUBJECTS.CSP_RULES_SHARED_VALUES}>
<CspInlineDescriptionList listItems={sharedValues} />
</div>
)
}
bottomBorder

View file

@ -105,6 +105,18 @@ describe('<Rules />', () => {
package: {
title: 'my package',
},
inputs: [
{
enabled: true,
policy_template: 'kspm',
type: 'cloudbeat/cis_k8s',
},
{
enabled: false,
policy_template: 'kspm',
type: 'cloudbeat/cis_eks',
},
],
},
{ name: 'my agent' },
],
@ -114,9 +126,7 @@ describe('<Rules />', () => {
render(<Component />);
expect(
await screen.findByText(`${response.data?.[0]?.package?.title}, ${response.data?.[1].name}`)
).toBeInTheDocument();
expect(await screen.findByTestId(TEST_SUBJECTS.CSP_RULES_CONTAINER)).toBeInTheDocument();
expect(await screen.findByTestId(TEST_SUBJECTS.CSP_RULES_SHARED_VALUES)).toBeInTheDocument();
});
});

View file

@ -6,6 +6,9 @@
*/
export const CSP_RULES_CONTAINER = 'csp_rules_container';
export const CSP_RULES_SHARED_VALUES = 'csp_rules_shared_values';
export const CSP_RULES_TABLE_ITEM_SWITCH = 'csp_rules_table_item_switch';
export const CSP_RULES_SAVE_BUTTON = 'csp_rules_table_save_button';
export const CSP_RULES_TABLE = 'csp_rules_table';
export const CSP_RULES_TABLE_ROW_ITEM_NAME = 'csp_rules_table_row_item_name';
export const CSP_RULES_FLYOUT_CONTAINER = 'csp_rules_flyout_container';

View file

@ -9535,10 +9535,8 @@
"xpack.csp.findings.findingsTableCell.addFilterButton": "Ajouter un filtre {field}",
"xpack.csp.findings.findingsTableCell.addNegateFilterButton": "Ajouter un filtre {field} négatif",
"xpack.csp.findings.latestFindings.bottomBarLabel": "Voici les {maxItems} premiers résultats correspondant à votre recherche. Veuillez l'affiner pour en voir davantage.",
"xpack.csp.findings.resourceFindings.resourceFindingsPageTitle": "{resourceId} - Résultats",
"xpack.csp.rules.header.rulesCountLabel": "{count, plural, one { règle} other { règles}}",
"xpack.csp.rules.header.totalRulesCount": "Affichage des {rules}",
"xpack.csp.rules.rulePageHeader.pageDescriptionTitle": "{integrationType}, {agentPolicyName}",
"xpack.csp.rules.rulePageHeader.pageHeaderTitle": "Règles - {integrationName}",
"xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundTitle": "Aucune intégration Benchmark trouvée",
"xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundWithFiltersTitle": "Nous n'avons trouvé aucune intégration Benchmark avec les filtres ci-dessus.",

View file

@ -9522,10 +9522,8 @@
"xpack.csp.findings.findingsTableCell.addFilterButton": "{field}フィルターを追加",
"xpack.csp.findings.findingsTableCell.addNegateFilterButton": "{field}否定フィルターを追加",
"xpack.csp.findings.latestFindings.bottomBarLabel": "これらは検索条件に一致した初めの{maxItems}件の調査結果です。他の結果を表示するには検索条件を絞ってください。",
"xpack.csp.findings.resourceFindings.resourceFindingsPageTitle": "{resourceId} - 調査結果",
"xpack.csp.rules.header.rulesCountLabel": "{count, plural, other {個のルール}}",
"xpack.csp.rules.header.totalRulesCount": "{rules}を表示しています",
"xpack.csp.rules.rulePageHeader.pageDescriptionTitle": "{integrationType}, {agentPolicyName}",
"xpack.csp.rules.rulePageHeader.pageHeaderTitle": "ルール - {integrationName}",
"xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundTitle": "ベンチマーク統合が見つかりません",
"xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundWithFiltersTitle": "上記のフィルターでベンチマーク統合が見つかりませんでした。",

View file

@ -9537,10 +9537,8 @@
"xpack.csp.findings.findingsTableCell.addFilterButton": "添加 {field} 筛选",
"xpack.csp.findings.findingsTableCell.addNegateFilterButton": "添加 {field} 作废筛选",
"xpack.csp.findings.latestFindings.bottomBarLabel": "这些是匹配您的搜索的前 {maxItems} 个结果,请优化搜索以查看其他结果。",
"xpack.csp.findings.resourceFindings.resourceFindingsPageTitle": "{resourceId} - 结果",
"xpack.csp.rules.header.rulesCountLabel": "{count, plural, other { 规则}}",
"xpack.csp.rules.header.totalRulesCount": "正在显示 {rules}",
"xpack.csp.rules.rulePageHeader.pageDescriptionTitle": "{integrationType}{agentPolicyName}",
"xpack.csp.rules.rulePageHeader.pageHeaderTitle": "规则 - {integrationName}",
"xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundTitle": "找不到基准集成",
"xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundWithFiltersTitle": "使用上述筛选,我们无法找到任何基准集成。",