diff --git a/x-pack/plugins/cloud_security_posture/common/types/index.ts b/x-pack/plugins/cloud_security_posture/common/types/index.ts index e53f34d5cf9193b..04fa2a95a8d5ee5 100644 --- a/x-pack/plugins/cloud_security_posture/common/types/index.ts +++ b/x-pack/plugins/cloud_security_posture/common/types/index.ts @@ -9,6 +9,7 @@ export * as rulesV1 from './rules/v1'; export * as rulesV2 from './rules/v2'; export * as rulesV3 from './rules/v3'; export * as rulesV4 from './rules/v4'; +export * as rulesV5 from './rules/v5'; export * as benchmarkV1 from './benchmarks/v1'; export * as benchmarkV2 from './benchmarks/v2'; diff --git a/x-pack/plugins/cloud_security_posture/common/types/latest.ts b/x-pack/plugins/cloud_security_posture/common/types/latest.ts index 73d86e76db2507c..32006fe5b5aef19 100644 --- a/x-pack/plugins/cloud_security_posture/common/types/latest.ts +++ b/x-pack/plugins/cloud_security_posture/common/types/latest.ts @@ -5,5 +5,5 @@ * 2.0. */ -export * from './rules/v4'; +export * from './rules/v5'; export * from './benchmarks/v2'; diff --git a/x-pack/plugins/cloud_security_posture/common/types/rules/v1.ts b/x-pack/plugins/cloud_security_posture/common/types/rules/v1.ts index afc2b705ab5c38f..068c7244ef4f61d 100644 --- a/x-pack/plugins/cloud_security_posture/common/types/rules/v1.ts +++ b/x-pack/plugins/cloud_security_posture/common/types/rules/v1.ts @@ -7,6 +7,9 @@ import { schema, TypeOf } from '@kbn/config-schema'; // Since version 8.3.0 + +export type CspBenchmarkRule = TypeOf; + export const cspBenchmarkRuleSchema = schema.object({ audit: schema.string(), benchmark: schema.object({ name: schema.string(), version: schema.string() }), @@ -26,5 +29,3 @@ export const cspBenchmarkRuleSchema = schema.object({ tags: schema.arrayOf(schema.string()), version: schema.string(), }); - -export type CspBenchmarkRule = TypeOf; diff --git a/x-pack/plugins/cloud_security_posture/common/types/rules/v2.ts b/x-pack/plugins/cloud_security_posture/common/types/rules/v2.ts index d88ae6adc089a3b..729de7736b0c090 100644 --- a/x-pack/plugins/cloud_security_posture/common/types/rules/v2.ts +++ b/x-pack/plugins/cloud_security_posture/common/types/rules/v2.ts @@ -7,6 +7,9 @@ import { schema, TypeOf } from '@kbn/config-schema'; // Since version 8.4.0 + +export type CspBenchmarkRule = TypeOf; + export const cspBenchmarkRuleMetadataSchema = schema.object({ audit: schema.string(), benchmark: schema.object({ @@ -34,5 +37,3 @@ export const cspBenchmarkRuleSchema = schema.object({ metadata: cspBenchmarkRuleMetadataSchema, muted: schema.boolean(), }); - -export type CspBenchmarkRule = TypeOf; diff --git a/x-pack/plugins/cloud_security_posture/common/types/rules/v3.ts b/x-pack/plugins/cloud_security_posture/common/types/rules/v3.ts index fb0310c088a1977..85c38c50022b84d 100644 --- a/x-pack/plugins/cloud_security_posture/common/types/rules/v3.ts +++ b/x-pack/plugins/cloud_security_posture/common/types/rules/v3.ts @@ -8,9 +8,18 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { CSPM_POLICY_TEMPLATE, KSPM_POLICY_TEMPLATE } from '../../constants'; -const DEFAULT_BENCHMARK_RULES_PER_PAGE = 25; +export const DEFAULT_BENCHMARK_RULES_PER_PAGE = 25; // Since version 8.7.0 + +export type FindCspBenchmarkRuleRequest = TypeOf; + +export type CspBenchmarkRuleMetadata = TypeOf; + +export type CspBenchmarkRule = TypeOf; + +export type PageUrlParams = Record<'policyId' | 'packagePolicyId', string>; + export const cspBenchmarkRuleMetadataSchema = schema.object({ audit: schema.string(), benchmark: schema.object({ @@ -37,14 +46,10 @@ export const cspBenchmarkRuleMetadataSchema = schema.object({ version: schema.string(), }); -export type CspBenchmarkRuleMetadata = TypeOf; - export const cspBenchmarkRuleSchema = schema.object({ metadata: cspBenchmarkRuleMetadataSchema, }); -export type CspBenchmarkRule = TypeOf; - export const findCspBenchmarkRuleRequestSchema = schema.object({ /** * An Elasticsearch simple_query_string @@ -125,13 +130,9 @@ export const findCspBenchmarkRuleRequestSchema = schema.object({ section: schema.maybe(schema.string()), }); -export type FindCspBenchmarkRuleRequest = TypeOf; - export interface FindCspBenchmarkRuleResponse { items: CspBenchmarkRule[]; total: number; page: number; perPage: number; } - -export type PageUrlParams = Record<'policyId' | 'packagePolicyId', string>; diff --git a/x-pack/plugins/cloud_security_posture/common/types/rules/v4.ts b/x-pack/plugins/cloud_security_posture/common/types/rules/v4.ts index 19711c7e7eb13f7..78680bf111dc752 100644 --- a/x-pack/plugins/cloud_security_posture/common/types/rules/v4.ts +++ b/x-pack/plugins/cloud_security_posture/common/types/rules/v4.ts @@ -7,6 +7,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { BenchmarksCisId } from '../latest'; +import { DEFAULT_BENCHMARK_RULES_PER_PAGE } from './v3'; export type { cspBenchmarkRuleMetadataSchema, CspBenchmarkRuleMetadata, @@ -15,7 +16,19 @@ export type { FindCspBenchmarkRuleResponse, } from './v3'; -const DEFAULT_BENCHMARK_RULES_PER_PAGE = 25; +export type FindCspBenchmarkRuleRequest = TypeOf; + +export type RulesToUpdate = TypeOf; + +export type CspBenchmarkRulesBulkActionRequestSchema = TypeOf< + typeof cspBenchmarkRulesBulkActionRequestSchema +>; + +export type RuleStateAttributes = TypeOf; + +export type CspBenchmarkRulesStates = TypeOf; + +export type CspSettings = TypeOf; export const findCspBenchmarkRuleRequestSchema = schema.object({ /** @@ -99,8 +112,6 @@ export const findCspBenchmarkRuleRequestSchema = schema.object({ ruleNumber: schema.maybe(schema.string()), }); -export type FindCspBenchmarkRuleRequest = TypeOf; - export interface BenchmarkRuleSelectParams { section?: string; ruleNumber?: string; @@ -125,12 +136,6 @@ export const cspBenchmarkRulesBulkActionRequestSchema = schema.object({ rules: rulesToUpdate, }); -export type RulesToUpdate = TypeOf; - -export type CspBenchmarkRulesBulkActionRequestSchema = TypeOf< - typeof cspBenchmarkRulesBulkActionRequestSchema ->; - export interface CspBenchmarkRulesBulkActionResponse { updated_benchmark_rules: CspBenchmarkRulesStates; disabled_detection_rules?: string[]; @@ -145,18 +150,12 @@ const ruleStateAttributes = schema.object({ rule_id: schema.string(), }); -export type RuleStateAttributes = TypeOf; - const rulesStates = schema.recordOf(schema.string(), ruleStateAttributes); -export type CspBenchmarkRulesStates = TypeOf; - export const cspSettingsSchema = schema.object({ rules: rulesStates, }); -export type CspSettings = TypeOf; - export interface BulkActionBenchmarkRulesResponse { updatedBenchmarkRulesStates: CspBenchmarkRulesStates; disabledDetectionRules: string[]; diff --git a/x-pack/plugins/cloud_security_posture/common/types/rules/v5.ts b/x-pack/plugins/cloud_security_posture/common/types/rules/v5.ts new file mode 100644 index 000000000000000..6f30ed446531aa5 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/common/types/rules/v5.ts @@ -0,0 +1,120 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; +import { DEFAULT_BENCHMARK_RULES_PER_PAGE } from './v3'; + +export type { + cspBenchmarkRuleMetadataSchema, + CspBenchmarkRuleMetadata, + cspBenchmarkRuleSchema, + CspBenchmarkRule, + FindCspBenchmarkRuleResponse, +} from './v3'; +export type { + PageUrlParams, + rulesToUpdate, + CspBenchmarkRulesBulkActionRequestSchema, + CspBenchmarkRulesBulkActionResponse, + RuleStateAttributes, + CspBenchmarkRulesStates, + cspSettingsSchema, + CspSettings, + BulkActionBenchmarkRulesResponse, +} from './v4'; + +export type FindCspBenchmarkRuleRequest = TypeOf; + +export const findCspBenchmarkRuleRequestSchema = schema.object({ + /** + * An Elasticsearch simple_query_string + */ + search: schema.maybe(schema.string()), + + /** + * The page of objects to return + */ + page: schema.number({ defaultValue: 1, min: 1 }), + + /** + * The number of objects to include in each page + */ + perPage: schema.number({ defaultValue: DEFAULT_BENCHMARK_RULES_PER_PAGE, min: 0 }), + + /** + * Fields to retrieve from CspBenchmarkRule saved object + */ + fields: schema.maybe(schema.arrayOf(schema.string())), + + /** + * The fields to perform the parsed query against. + * Valid fields are fields which mapped to 'text' in cspBenchmarkRuleSavedObjectMapping + */ + searchFields: schema.arrayOf( + schema.oneOf([schema.literal('metadata.name.text'), schema.literal('metadata.section.text')]), + { defaultValue: ['metadata.name.text'] } + ), + + /** + * Sort Field + */ + sortField: schema.oneOf( + [ + schema.literal('metadata.name'), + schema.literal('metadata.section'), + schema.literal('metadata.id'), + schema.literal('metadata.version'), + schema.literal('metadata.benchmark.id'), + schema.literal('metadata.benchmark.name'), + schema.literal('metadata.benchmark.posture_type'), + schema.literal('metadata.benchmark.version'), + schema.literal('metadata.benchmark.rule_number'), + ], + { + defaultValue: 'metadata.benchmark.rule_number', + } + ), + + /** + * The order to sort by + */ + sortOrder: schema.oneOf([schema.literal('asc'), schema.literal('desc')], { + defaultValue: 'asc', + }), + + /** + * benchmark id + */ + benchmarkId: schema.maybe( + schema.oneOf([ + schema.literal('cis_k8s'), + schema.literal('cis_eks'), + schema.literal('cis_aws'), + schema.literal('cis_azure'), + schema.literal('cis_gcp'), + ]) + ), + + /** + * benchmark version + */ + benchmarkVersion: schema.maybe(schema.string()), + + /** + * rule section + */ + section: schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.string(), { minSize: 1 })]) + ), + ruleNumber: schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.string(), { minSize: 1 })]) + ), +}); + +export interface BenchmarkRuleSelectParams { + section?: string[]; + ruleNumber?: string[]; +} diff --git a/x-pack/plugins/cloud_security_posture/common/utils/helpers.test.ts b/x-pack/plugins/cloud_security_posture/common/utils/helpers.test.ts index 6ce2add754f2006..951800d8a3cc03e 100644 --- a/x-pack/plugins/cloud_security_posture/common/utils/helpers.test.ts +++ b/x-pack/plugins/cloud_security_posture/common/utils/helpers.test.ts @@ -6,7 +6,12 @@ */ import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; -import { getBenchmarkFromPackagePolicy, getBenchmarkFilter, cleanupCredentials } from './helpers'; +import { + getBenchmarkFromPackagePolicy, + getBenchmarkFilter, + cleanupCredentials, + getBenchmarkFilterQueryV2, +} from './helpers'; describe('test helper methods', () => { it('get default integration type from inputs with multiple enabled types', () => { @@ -70,6 +75,44 @@ describe('test helper methods', () => { ); }); + it('get benchmark filter query based on a benchmark Id, version', () => { + const typeFilter = getBenchmarkFilterQueryV2('cis_eks', '1.0.1'); + expect(typeFilter).toMatch( + 'csp-rule-template.attributes.metadata.benchmark.id:cis_eks AND csp-rule-template.attributes.metadata.benchmark.version:"v1.0.1"' + ); + }); + + it('get benchmark filter query based on a benchmark Id, version and multiple sections and rule numbers', () => { + const mockSelectParams = { + section: ['section_1', 'section_2'], + ruleNumber: ['1a', '2b', '3c'], + }; + const typeFilter = getBenchmarkFilterQueryV2('cis_eks', '1.0.1', mockSelectParams); + expect(typeFilter).toMatch( + 'csp-rule-template.attributes.metadata.benchmark.id:cis_eks AND csp-rule-template.attributes.metadata.benchmark.version:"v1.0.1" AND (csp-rule-template.attributes.metadata.section: "section_1" OR csp-rule-template.attributes.metadata.section: "section_2") AND (csp-rule-template.attributes.metadata.benchmark.rule_number: "1a" OR csp-rule-template.attributes.metadata.benchmark.rule_number: "2b" OR csp-rule-template.attributes.metadata.benchmark.rule_number: "3c")' + ); + }); + + it('get benchmark filter query based on a benchmark Id, version and just sections', () => { + const mockSelectParams = { + section: ['section_1', 'section_2'], + }; + const typeFilter = getBenchmarkFilterQueryV2('cis_eks', '1.0.1', mockSelectParams); + expect(typeFilter).toMatch( + 'csp-rule-template.attributes.metadata.benchmark.id:cis_eks AND csp-rule-template.attributes.metadata.benchmark.version:"v1.0.1" AND (csp-rule-template.attributes.metadata.section: "section_1" OR csp-rule-template.attributes.metadata.section: "section_2")' + ); + }); + + it('get benchmark filter query based on a benchmark Id, version and just rule numbers', () => { + const mockSelectParams = { + ruleNumber: ['1a', '2b', '3c'], + }; + const typeFilter = getBenchmarkFilterQueryV2('cis_eks', '1.0.1', mockSelectParams); + expect(typeFilter).toMatch( + 'csp-rule-template.attributes.metadata.benchmark.id:cis_eks AND csp-rule-template.attributes.metadata.benchmark.version:"v1.0.1" AND (csp-rule-template.attributes.metadata.benchmark.rule_number: "1a" OR csp-rule-template.attributes.metadata.benchmark.rule_number: "2b" OR csp-rule-template.attributes.metadata.benchmark.rule_number: "3c")' + ); + }); + describe('cleanupCredentials', () => { it('cleans unused aws credential methods, except role_arn when using assume_role', () => { const mockPackagePolicy = createPackagePolicyMock(); diff --git a/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts b/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts index 3c70b3a7964b94b..0dc376b19f17954 100644 --- a/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts +++ b/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts @@ -32,6 +32,7 @@ import type { RuleSection, } from '../types_old'; import type { BenchmarkRuleSelectParams, BenchmarksCisId } from '../types/latest'; +import type { BenchmarkRuleSelectParams as BenchmarkRuleSelectParamsV1 } from '../types/rules/v4'; /** * @example @@ -205,11 +206,11 @@ export const getBenchmarkApplicableTo = (benchmarkId: BenchmarksCisId) => { }; export const getBenchmarkFilterQuery = ( - id: BenchmarkId, - version?: string, - selectParams?: BenchmarkRuleSelectParams + benchmarkId: BenchmarkId, + benchmarkVersion?: string, + selectParams?: BenchmarkRuleSelectParamsV1 ): string => { - const baseQuery = `${CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE}.attributes.metadata.benchmark.id:${id} AND ${CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE}.attributes.metadata.benchmark.version:"v${version}"`; + const baseQuery = `${CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE}.attributes.metadata.benchmark.id:${benchmarkId} AND ${CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE}.attributes.metadata.benchmark.version:"v${benchmarkVersion}"`; const sectionQuery = selectParams?.section ? ` AND ${CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE}.attributes.metadata.section: "${selectParams.section}"` : ''; @@ -218,3 +219,29 @@ export const getBenchmarkFilterQuery = ( : ''; return baseQuery + sectionQuery + ruleNumberQuery; }; + +export const getBenchmarkFilterQueryV2 = ( + benchmarkId: BenchmarkId, + benchmarkVersion?: string, + selectParams?: BenchmarkRuleSelectParams +): string => { + const baseQuery = `${CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE}.attributes.metadata.benchmark.id:${benchmarkId} AND ${CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE}.attributes.metadata.benchmark.version:"v${benchmarkVersion}"`; + + let sectionQuery = ''; + let ruleNumberQuery = ''; + if (selectParams?.section) { + const sectionParamsArr = selectParams.section?.map( + (params) => `${CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE}.attributes.metadata.section: "${params}"` + ); + sectionQuery = ' AND (' + sectionParamsArr.join(' OR ') + ')'; + } + if (selectParams?.ruleNumber) { + const ruleNumbersParamsArr = selectParams.ruleNumber?.map( + (params) => + `${CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE}.attributes.metadata.benchmark.rule_number: "${params}"` + ); + ruleNumberQuery = ' AND (' + ruleNumbersParamsArr.join(' OR ') + ')'; + } + + return baseQuery + sectionQuery + ruleNumberQuery; +}; diff --git a/x-pack/plugins/cloud_security_posture/public/common/component/multi_select_filter.tsx b/x-pack/plugins/cloud_security_posture/public/common/component/multi_select_filter.tsx new file mode 100644 index 000000000000000..6eece8a31c5e899 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/component/multi_select_filter.tsx @@ -0,0 +1,200 @@ +/* + * 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. + */ + +/* This code is based on MultiSelectFilter component from x-pack/plugins/cases/public/components/all_cases/multi_select_filter.tsx */ +import React, { useState, useEffect } from 'react'; +import { css } from '@emotion/react'; +import type { EuiSelectableOption } from '@elastic/eui'; +import { + EuiPopoverTitle, + EuiCallOut, + EuiHorizontalRule, + EuiPopover, + EuiSelectable, + EuiFilterButton, + EuiFilterGroup, + EuiText, +} from '@elastic/eui'; +import { isEqual } from 'lodash/fp'; +import { i18n } from '@kbn/i18n'; + +type FilterOption = EuiSelectableOption<{ + key: K; + label: T; +}>; + +export type { FilterOption as MultiSelectFilterOption }; + +export const mapToMultiSelectOption = (options: T[]) => { + return options.map((option) => { + return { + key: option, + label: option, + }; + }); +}; + +const fromRawOptionsToEuiSelectableOptions = ( + options: Array>, + selectedOptionKeys: string[] +): Array> => { + return options.map(({ key, label }) => { + const selectableOption: FilterOption = { label, key }; + if (selectedOptionKeys.includes(key)) { + selectableOption.checked = 'on'; + } + selectableOption['data-test-subj'] = `options-filter-popover-item-${key.split(' ').join('-')}`; + return selectableOption; + }); +}; + +const fromEuiSelectableOptionToRawOption = ( + options: Array> +): string[] => { + return options.map((option) => option.key); +}; + +const getEuiSelectableCheckedOptions = ( + options: Array> +) => options.filter((option) => option.checked === 'on') as Array>; + +interface UseFilterParams { + buttonIconType?: string; + buttonLabel?: string; + hideActiveOptionsNumber?: boolean; + id: string; + limit?: number; + limitReachedMessage?: string; + onChange: (params: { filterId: string; selectedOptionKeys: string[] }) => void; + options: Array>; + renderOption?: (option: FilterOption) => React.ReactNode; + selectedOptionKeys?: string[]; + transparentBackground?: boolean; +} +export const MultiSelectFilter = ({ + buttonLabel, + buttonIconType, + hideActiveOptionsNumber, + id, + limit, + limitReachedMessage, + onChange, + options: rawOptions, + selectedOptionKeys = [], + renderOption, + transparentBackground, +}: UseFilterParams) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const toggleIsPopoverOpen = () => setIsPopoverOpen((prevValue) => !prevValue); + const showActiveOptionsNumber = !hideActiveOptionsNumber; + const isInvalid = Boolean(limit && limitReachedMessage && selectedOptionKeys.length >= limit); + const options = fromRawOptionsToEuiSelectableOptions(rawOptions, selectedOptionKeys); + + useEffect(() => { + const newSelectedOptions = selectedOptionKeys.filter((selectedOptionKey) => + rawOptions.some(({ key: optionKey }) => optionKey === selectedOptionKey) + ); + if (!isEqual(newSelectedOptions, selectedOptionKeys)) { + onChange({ + filterId: id, + selectedOptionKeys: newSelectedOptions, + }); + } + }, [selectedOptionKeys, rawOptions, id, onChange]); + + const _onChange = (newOptions: Array>) => { + const newSelectedOptions = getEuiSelectableCheckedOptions(newOptions); + if (isInvalid && limit && newSelectedOptions.length >= limit) { + return; + } + + onChange({ + filterId: id, + selectedOptionKeys: fromEuiSelectableOptionToRawOption(newSelectedOptions), + }); + }; + + return ( + + 0 : undefined} + numActiveFilters={showActiveOptionsNumber ? selectedOptionKeys.length : undefined} + aria-label={buttonLabel} + > + + {buttonLabel} + + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + panelPaddingSize="none" + repositionOnScroll + > + {isInvalid && ( + <> + + + + + )} + > + options={options} + searchable + searchProps={{ + placeholder: + i18n.translate('xpack.csp.common.component.multiSelectFilter.searchWord', { + defaultMessage: 'Search ', + }) + buttonLabel, + compressed: false, + 'data-test-subj': `${id}-search-input`, + css: css` + border-radius: 0px !important; + `, + }} + emptyMessage={'empty'} + onChange={_onChange} + singleSelection={false} + renderOption={renderOption} + > + {(list, search) => ( +
+ {search} + {list} +
+ )} + +
+
+ ); +}; + +MultiSelectFilter.displayName = 'MultiSelectFilter'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx index a6853a20db3ade3..e0017fa7a54e137 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx @@ -5,6 +5,7 @@ * 2.0. */ import React, { useState, useMemo } from 'react'; +import compareVersions from 'compare-versions'; import { EuiSpacer } from '@elastic/eui'; import { useParams } from 'react-router-dom'; import { buildRuleKey } from '../../../common/utils/rules_states'; @@ -117,7 +118,7 @@ export const RulesContainer = () => { const rulesKey = buildRuleKey( rule.metadata.benchmark.id, rule.metadata.benchmark.version, - /* Since Packages are automatically upgraded, we can be sure that rule_number will Always exist */ + /* Rule number always exists* from 8.7 */ rule.metadata.benchmark.rule_number! ); @@ -147,7 +148,7 @@ export const RulesContainer = () => { const cleanedSectionList = [...new Set(sectionList)].sort((a, b) => { return a.localeCompare(b, 'en', { sensitivity: 'base' }); }); - const cleanedRuleNumberList = [...new Set(ruleNumberList)]; + const cleanedRuleNumberList = [...new Set(ruleNumberList)].sort(compareVersions); const rulesPageData = useMemo( () => getRulesPage(filteredRulesWithStates, status, error, rulesQuery), diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx index 44bf8cc62785ad1..b792ab851fa7487 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx @@ -14,6 +14,8 @@ import { useEuiTheme, EuiSwitch, EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { uniqBy } from 'lodash'; @@ -190,7 +192,7 @@ const getColumns = ({ }} /> ), - width: '30px', + width: '40px', sortable: false, render: (rules, item: CspBenchmarkRulesWithStates) => { return ( @@ -219,7 +221,7 @@ const getColumns = ({ name: i18n.translate('xpack.csp.rules.rulesTable.ruleNumberColumnLabel', { defaultMessage: 'Rule Number', }), - width: '10%', + width: '100px', sortable: true, }, { @@ -248,20 +250,21 @@ const getColumns = ({ name: i18n.translate('xpack.csp.rules.rulesTable.cisSectionColumnLabel', { defaultMessage: 'CIS Section', }), - width: '15%', + width: '24%', }, { field: 'metadata.name', name: i18n.translate('xpack.csp.rules.rulesTable.mutedColumnLabel', { defaultMessage: 'Enabled', }), - width: '10%', + align: 'right', + width: '100px', truncateText: true, render: (name, rule: CspBenchmarkRulesWithStates) => { const rulesObjectRequest = { benchmark_id: rule?.metadata.benchmark.id, benchmark_version: rule?.metadata.benchmark.version, - /* Since Packages are automatically upgraded, we can be sure that rule_number will Always exist */ + /* Rule number always exists from 8.7 */ rule_number: rule?.metadata.benchmark.rule_number!, rule_id: rule?.metadata.id, }; @@ -275,13 +278,18 @@ const getColumns = ({ } }; return ( - + + + + + ); }, }, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx index a960d541f16309b..366ae740c3e94d9 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx @@ -6,13 +6,11 @@ */ import React, { useState } from 'react'; import { - EuiComboBox, EuiFieldSearch, EuiFlexItem, EuiText, EuiSpacer, EuiFlexGroup, - type EuiComboBoxOptionOption, EuiPopover, EuiButtonEmpty, EuiContextMenuItem, @@ -28,6 +26,7 @@ import { css } from '@emotion/react'; import { euiThemeVars } from '@kbn/ui-theme'; import { RuleStateAttributesWithoutStates, useChangeCspRuleState } from './change_csp_rule_state'; import { CspBenchmarkRulesWithStates } from './rules_container'; +import { MultiSelectFilter } from '../../common/component/multi_select_filter'; export const RULES_BULK_ACTION_BUTTON = 'bulk-action-button'; export const RULES_BULK_ACTION_OPTION_ENABLE = 'bulk-action-option-enable'; @@ -36,11 +35,13 @@ export const RULES_SELECT_ALL_RULES = 'select-all-rules-button'; export const RULES_CLEAR_ALL_RULES_SELECTION = 'clear-rules-selection-button'; export const RULES_DISABLED_FILTER = 'rules-disabled-filter'; export const RULES_ENABLED_FILTER = 'rules-enabled-filter'; +export const CIS_SECTION_FILTER = 'cis-section-filter'; +export const RULE_NUMBER_FILTER = 'rule-number-filter'; interface RulesTableToolbarProps { search: (value: string) => void; - onSectionChange: (value: string | undefined) => void; - onRuleNumberChange: (value: string | undefined) => void; + onSectionChange: (value: string[] | undefined) => void; + onRuleNumberChange: (value: string[] | undefined) => void; sectionSelectOptions: string[]; ruleNumberSelectOptions: string[]; totalRulesCount: number; @@ -81,16 +82,16 @@ export const RulesTableHeader = ({ setSelectAllRules, setSelectedRules, }: RulesTableToolbarProps) => { - const [selectedSection, setSelectedSection] = useState([]); - const [selectedRuleNumber, setSelectedRuleNumber] = useState([]); + const [selectedSection, setSelectedSection] = useState([]); + const [selectedRuleNumber, setSelectedRuleNumber] = useState([]); const sectionOptions = sectionSelectOptions.map((option) => ({ + key: option, label: option, })); - const ruleNumberOptions = ruleNumberSelectOptions.map((option) => ({ + key: option, label: option, })); - const [isEnabledRulesFilterOn, setIsEnabledRulesFilterOn] = useState(false); const [isDisabledRulesFilterOn, setisDisabledRulesFilterOn] = useState(false); @@ -130,27 +131,28 @@ export const RulesTableHeader = ({ /> - + - { - setSelectedSection(option); - onSectionChange(option.length ? option[0].label : undefined); + id={'cis-section-multi-select-filter'} + onChange={(section) => { + setSelectedSection([...section?.selectedOptionKeys]); + onSectionChange( + section?.selectedOptionKeys ? section?.selectedOptionKeys : undefined + ); }} + options={sectionOptions} + selectedOptionKeys={selectedSection} /> - { - setSelectedRuleNumber(option); - onRuleNumberChange(option.length ? option[0].label : undefined); + id={'rule-number-multi-select-filter'} + onChange={(ruleNumber) => { + setSelectedRuleNumber([...ruleNumber?.selectedOptionKeys]); + onRuleNumberChange( + ruleNumber?.selectedOptionKeys ? ruleNumber?.selectedOptionKeys : undefined + ); }} + options={ruleNumberOptions} + selectedOptionKeys={selectedRuleNumber} /> { return http.get(FIND_CSP_BENCHMARK_RULE_ROUTE_PATH, { query: { benchmarkId, page, perPage, search, section, benchmarkVersion, ruleNumber }, - version: '2', + version: '3', }); } ); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules_state.ts b/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules_state.ts index ff56ad8ad4570c7..da34cf8b247c761 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules_state.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules_state.ts @@ -10,7 +10,7 @@ import { CspBenchmarkRulesStates } from '../../../common/types/latest'; import { CSP_GET_BENCHMARK_RULES_STATE_ROUTE_PATH } from '../../../common/constants'; import { useKibana } from '../../common/hooks/use_kibana'; -const QUERY_KEY_V1 = 'csp_rules_status_v1'; +const QUERY_KEY_V1 = 'csp_rules_states_v1'; export const useCspGetRulesStates = () => { const { http } = useKibana().services; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.ts index 12954073e552a5b..8dc8f36554600f6 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.ts @@ -14,10 +14,15 @@ import { FindCspBenchmarkRuleRequest as FindCspBenchmarkRuleRequestV1, findCspBenchmarkRuleRequestSchema as findCspBenchmarkRuleRequestSchemaV1, } from '../../../../common/types/rules/v3'; +import { + FindCspBenchmarkRuleRequest as FindCspBenchmarkRuleRequestV2, + findCspBenchmarkRuleRequestSchema as findCspBenchmarkRuleRequestSchemaV2, +} from '../../../../common/types/rules/v4'; import { FIND_CSP_BENCHMARK_RULE_ROUTE_PATH } from '../../../../common/constants'; import { CspRouter } from '../../../types'; import { findBenchmarkRuleHandler as findRuleHandlerV1 } from './v1'; import { findBenchmarkRuleHandler as findRuleHandlerV2 } from './v2'; +import { findBenchmarkRuleHandler as findRuleHandlerV3 } from './v3'; export const defineFindCspBenchmarkRuleRoute = (router: CspRouter) => router.versioned @@ -61,6 +66,40 @@ export const defineFindCspBenchmarkRuleRoute = (router: CspRouter) => .addVersion( { version: '2', + validate: { + request: { + query: findCspBenchmarkRuleRequestSchemaV2, + }, + }, + }, + async (context, request, response) => { + if (!(await context.fleet).authz.fleet.all) { + return response.forbidden(); + } + + const requestBody: FindCspBenchmarkRuleRequestV2 = request.query; + const cspContext = await context.csp; + + try { + const cspBenchmarkRules: FindCspBenchmarkRuleResponse = await findRuleHandlerV2( + cspContext.soClient, + requestBody + ); + + return response.ok({ body: cspBenchmarkRules }); + } catch (err) { + const error = transformError(err); + cspContext.logger.error(`Failed to fetch csp rules templates ${err}`); + return response.customError({ + body: { message: error.message }, + statusCode: error.statusCode, + }); + } + } + ) + .addVersion( + { + version: '3', validate: { request: { query: findCspBenchmarkRuleRequestSchema, @@ -76,7 +115,7 @@ export const defineFindCspBenchmarkRuleRoute = (router: CspRouter) => const cspContext = await context.csp; try { - const cspBenchmarkRules: FindCspBenchmarkRuleResponse = await findRuleHandlerV2( + const cspBenchmarkRules: FindCspBenchmarkRuleResponse = await findRuleHandlerV3( cspContext.soClient, requestBody ); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v2.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v2.ts index 228d7d5e9c3e6b2..5054fc211a5292d 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v2.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v2.ts @@ -13,7 +13,7 @@ import type { CspBenchmarkRule, FindCspBenchmarkRuleRequest, FindCspBenchmarkRuleResponse, -} from '../../../../common/types/latest'; +} from '../../../../common/types/rules/v4'; import { getSortedCspBenchmarkRulesTemplates } from './utils'; export const findBenchmarkRuleHandler = async ( diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v3.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v3.ts new file mode 100644 index 000000000000000..6cf4efd5705a0ee --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/v3.ts @@ -0,0 +1,58 @@ +/* + * 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 { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import { getBenchmarkFilterQueryV2 } from '../../../../common/utils/helpers'; +import { CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE } from '../../../../common/constants'; + +import type { + CspBenchmarkRule, + FindCspBenchmarkRuleRequest, + FindCspBenchmarkRuleResponse, +} from '../../../../common/types/latest'; +import { getSortedCspBenchmarkRulesTemplates } from './utils'; + +export const findBenchmarkRuleHandler = async ( + soClient: SavedObjectsClientContract, + options: FindCspBenchmarkRuleRequest +): Promise => { + if (!options.benchmarkId) { + throw new Error('Please provide benchmarkId'); + } + const sectionFilter: string[] | undefined = + typeof options?.section === 'string' ? [options?.section] : options?.section; + const ruleNumberFilter: string[] | undefined = + typeof options?.ruleNumber === 'string' ? [options?.ruleNumber] : options?.ruleNumber; + const benchmarkId = options.benchmarkId; + const cspCspBenchmarkRulesSo = await soClient.find({ + type: CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE, + searchFields: options.searchFields, + search: options.search ? `"${options.search}"*` : '', + page: options.page, + perPage: options.perPage, + sortField: options.sortField, + fields: options?.fields, + filter: getBenchmarkFilterQueryV2(benchmarkId, options.benchmarkVersion || '', { + section: sectionFilter, + ruleNumber: ruleNumberFilter, + }), + }); + + const cspBenchmarkRules = cspCspBenchmarkRulesSo.saved_objects.map( + (cspBenchmarkRule) => cspBenchmarkRule.attributes + ); + + // Semantic version sorting using semver for valid versions and custom comparison for invalid versions + const sortedCspBenchmarkRules = getSortedCspBenchmarkRulesTemplates(cspBenchmarkRules); + + return { + items: sortedCspBenchmarkRules, + total: cspCspBenchmarkRulesSo.total, + page: options.page, + perPage: options.perPage, + }; +}; diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/rule_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/rule_page.ts index 3136b2b240fb45e..7547bf67be8ab26 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/rule_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/rule_page.ts @@ -20,6 +20,9 @@ export const RULES_CLEAR_ALL_RULES_SELECTION = 'clear-rules-selection-button'; export const RULES_ROWS_ENABLE_SWITCH_BUTTON = 'rules-row-enable-switch-button'; export const RULES_DISABLED_FILTER = 'rules-disabled-filter'; export const RULES_ENABLED_FILTER = 'rules-enabled-filter'; +export const CIS_SECTION_FILTER = 'options-filter-popover-button-cis-section-multi-select-filter'; +export const RULE_NUMBER_FILTER = 'options-filter-popover-button-rule-number-multi-select-filter'; +export const RULE_NUMBER_FILTER_SEARCH_FIELD = 'rule-number-search-input'; export function RulePagePageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); @@ -86,6 +89,27 @@ export function RulePagePageProvider({ getService, getPageObjects }: FtrProvider const enableRulesRowSwitch = await testSubjects.findAll(RULES_ROWS_ENABLE_SWITCH_BUTTON); return await enableRulesRowSwitch.length; }, + + clickFilterPopover: async (filterType: 'section' | 'ruleNumber') => { + const filterPopoverButton = + (await filterType) === 'section' + ? await testSubjects.find(CIS_SECTION_FILTER) + : await testSubjects.find(RULE_NUMBER_FILTER); + + await filterPopoverButton.click(); + await PageObjects.header.waitUntilLoadingHasFinished(); + }, + + clickFilterPopOverOption: async (value: string) => { + const chosenValue = await testSubjects.find('options-filter-popover-item-' + value); + await chosenValue.click(); + }, + + filterTextInput: async (selector: string, value: string) => { + const textField = await testSubjects.find(selector); + await textField.type(value); + await PageObjects.header.waitUntilLoadingHasFinished(); + }, }; const navigateToRulePage = async (benchmarkCisId: string, benchmarkCisVersion: string) => { @@ -94,6 +118,7 @@ export function RulePagePageProvider({ getService, getPageObjects }: FtrProvider `cloud_security_posture/benchmarks/${benchmarkCisId}/${benchmarkCisVersion}/rules`, { shouldUseHashForSubUrl: false } ); + await PageObjects.header.waitUntilLoadingHasFinished(); }; return { diff --git a/x-pack/test/cloud_security_posture_functional/pages/rules.ts b/x-pack/test/cloud_security_posture_functional/pages/rules.ts index 46859f75572d8ff..4841c9ad6b8b341 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/rules.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/rules.ts @@ -27,8 +27,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'findings', ]); - describe('Cloud Posture Dashboard Page', function () { - this.tags(['cloud_security_posture_compliance_dashboard']); + describe('Cloud Posture Rules Page', function () { + this.tags(['cloud_security_posture_rules_page']); let rule: typeof pageObjects.rule; let agentPolicyId: string; @@ -59,7 +59,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); await rule.waitForPluginInitialized(); await rule.navigateToRulePage('cis_k8s', '1.0.1'); - await pageObjects.header.waitUntilLoadingHasFinished(); }); afterEach(async () => { @@ -139,5 +138,30 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect((await rule.rulePage.getEnableRulesRowSwitchButton()) > 1).to.be(true); }); }); + + describe('Rules Page - CIS Section & Rule Number filters', () => { + it('Table should only show result that has the same section as in the Section filter', async () => { + await rule.rulePage.clickFilterPopover('section'); + await rule.rulePage.clickFilterPopOverOption('etcd'); + await rule.rulePage.clickFilterPopOverOption('Scheduler'); + expect((await rule.rulePage.getEnableRulesRowSwitchButton()) < 10).to.be(true); + }); + + it('Table should only show result that has the same section as in the Rule number filter', async () => { + await rule.rulePage.clickFilterPopover('ruleNumber'); + await rule.rulePage.clickFilterPopOverOption('1.1.1'); + await rule.rulePage.clickFilterPopOverOption('1.1.2'); + expect((await rule.rulePage.getEnableRulesRowSwitchButton()) === 2).to.be(true); + }); + + it('Table should only show result that passes both Section and Rule number filter', async () => { + await rule.rulePage.clickFilterPopover('section'); + await rule.rulePage.clickFilterPopOverOption('Control-Plane-Node-Configuration-Files'); + await rule.rulePage.clickFilterPopover('section'); + await rule.rulePage.clickFilterPopover('ruleNumber'); + await rule.rulePage.clickFilterPopOverOption('1.1.5'); + expect((await rule.rulePage.getEnableRulesRowSwitchButton()) === 1).to.be(true); + }); + }); }); }