diff --git a/x-pack/packages/kbn-cloud-security-posture/common/utils/ui_metrics.ts b/x-pack/packages/kbn-cloud-security-posture/common/utils/ui_metrics.ts index c7baeb47bc214..e38af0a901393 100644 --- a/x-pack/packages/kbn-cloud-security-posture/common/utils/ui_metrics.ts +++ b/x-pack/packages/kbn-cloud-security-posture/common/utils/ui_metrics.ts @@ -27,9 +27,9 @@ export const ENTITY_FLYOUT_EXPAND_MISCONFIGURATION_VIEW_VISITS = 'entity-flyout-expand-misconfiguration-view-visits'; export const ENTITY_FLYOUT_EXPAND_VULNERABILITY_VIEW_VISITS = 'entity-flyout-expand-vulnerability-view-visits'; -export const NAV_TO_FINDINGS_BY_HOST_NAME_FRPOM_ENTITY_FLYOUT = +export const NAV_TO_FINDINGS_BY_HOST_NAME_FROM_ENTITY_FLYOUT = 'nav-to-findings-by-host-name-from-entity-flyout'; -export const NAV_TO_FINDINGS_BY_RULE_NAME_FRPOM_ENTITY_FLYOUT = +export const NAV_TO_FINDINGS_BY_RULE_NAME_FROM_ENTITY_FLYOUT = 'nav-to-findings-by-rule-name-from-entity-flyout'; export const CREATE_DETECTION_RULE_FROM_FLYOUT = 'create-detection-rule-from-flyout'; export const CREATE_DETECTION_FROM_TABLE_ROW_ACTION = 'create-detection-from-table-row-action'; @@ -37,20 +37,22 @@ export const VULNERABILITIES_FLYOUT_VISITS = 'vulnerabilities-flyout-visits'; export const OPEN_FINDINGS_FLYOUT = 'open-findings-flyout'; export const GROUP_BY_CLICK = 'group-by-click'; export const CHANGE_RULE_STATE = 'change-rule-state'; +export const CHANGE_MULTIPLE_RULE_STATE = 'change-multiple-rule-state'; type CloudSecurityUiCounters = | typeof ENTITY_FLYOUT_WITH_MISCONFIGURATION_VISIT | typeof ENTITY_FLYOUT_WITH_VULNERABILITY_PREVIEW | typeof ENTITY_FLYOUT_EXPAND_MISCONFIGURATION_VIEW_VISITS | typeof ENTITY_FLYOUT_EXPAND_VULNERABILITY_VIEW_VISITS - | typeof NAV_TO_FINDINGS_BY_HOST_NAME_FRPOM_ENTITY_FLYOUT - | typeof NAV_TO_FINDINGS_BY_RULE_NAME_FRPOM_ENTITY_FLYOUT + | typeof NAV_TO_FINDINGS_BY_HOST_NAME_FROM_ENTITY_FLYOUT + | typeof NAV_TO_FINDINGS_BY_RULE_NAME_FROM_ENTITY_FLYOUT | typeof VULNERABILITIES_FLYOUT_VISITS | typeof OPEN_FINDINGS_FLYOUT | typeof CREATE_DETECTION_RULE_FROM_FLYOUT | typeof CREATE_DETECTION_FROM_TABLE_ROW_ACTION | typeof GROUP_BY_CLICK | typeof CHANGE_RULE_STATE + | typeof CHANGE_MULTIPLE_RULE_STATE | typeof MISCONFIGURATION_INSIGHT_HOST_DETAILS | typeof MISCONFIGURATION_INSIGHT_USER_DETAILS | typeof MISCONFIGURATION_INSIGHT_HOST_ENTITY_OVERVIEW diff --git a/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx b/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx index 8c7bcb5886d8d..7b61eadc67ae6 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx @@ -37,8 +37,8 @@ const RULE_PAGE_PATH = '/app/security/rules/id/'; interface TakeActionProps { createRuleFn?: (http: HttpSetup) => Promise; - enableBenchmarkRuleFn?: () => Promise; - disableBenchmarkRuleFn?: () => Promise; + enableBenchmarkRuleFn?: () => void; + disableBenchmarkRuleFn?: () => void; isCreateDetectionRuleDisabled?: boolean; isDataGridControlColumn?: boolean; } @@ -263,7 +263,7 @@ const EnableBenchmarkRule = ({ setIsLoading, closePopover, }: { - enableBenchmarkRuleFn: () => Promise; + enableBenchmarkRuleFn: () => void; setIsLoading: (isLoading: boolean) => void; closePopover: () => void; }) => { @@ -288,7 +288,7 @@ const DisableBenchmarkRule = ({ setIsLoading, closePopover, }: { - disableBenchmarkRuleFn: () => Promise; + disableBenchmarkRuleFn: () => void; setIsLoading: (isLoading: boolean) => void; closePopover: () => void; }) => { 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 8eaec34adf1cf..aa18ef124d325 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 @@ -4,73 +4,21 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState, useMemo, useEffect } from 'react'; +import React from 'react'; import { EuiSpacer } from '@elastic/eui'; import { useParams, useHistory, generatePath } from 'react-router-dom'; -import type { - CspBenchmarkRule, - PageUrlParams, - RuleStateAttributes, -} from '@kbn/cloud-security-posture-common/schema/rules/latest'; -import { extractErrorMessage } from '@kbn/cloud-security-posture-common'; -import semVerCompare from 'semver/functions/compare'; -import semVerCoerce from 'semver/functions/coerce'; +import type { PageUrlParams } from '@kbn/cloud-security-posture-common/schema/rules/latest'; import { benchmarksNavigation } from '../../common/navigation/constants'; -import { buildRuleKey } from '../../../common/utils/rules_states'; import { RulesTable } from './rules_table'; import { RulesTableHeader } from './rules_table_header'; -import { useFindCspBenchmarkRule, type RulesQuery } from './use_csp_benchmark_rules'; import * as TEST_SUBJECTS from './test_subjects'; import { RuleFlyout } from './rules_flyout'; -import { LOCAL_STORAGE_PAGE_SIZE_RULES_KEY } from '../../common/constants'; -import { usePageSize } from '../../common/hooks/use_page_size'; -import { useCspGetRulesStates } from './use_csp_rules_state'; import { RulesCounters } from './rules_counters'; - -export interface CspBenchmarkRulesWithStates { - metadata: CspBenchmarkRule['metadata']; - state: 'muted' | 'unmuted'; -} - -interface RulesPageData { - rules_page: CspBenchmarkRulesWithStates[]; - all_rules: CspBenchmarkRulesWithStates[]; - rules_map: Map; - total: number; - error?: string; - loading: boolean; -} - -export type RulesState = RulesPageData & RulesQuery; - -const getPage = (data: CspBenchmarkRulesWithStates[], { page, perPage }: RulesQuery) => - data.slice(page * perPage, (page + 1) * perPage); - -const getRulesPageData = ( - data: CspBenchmarkRulesWithStates[], - status: string, - error: unknown, - query: RulesQuery -): RulesPageData => { - const page = getPage(data, query); - - return { - loading: status === 'loading', - error: error ? extractErrorMessage(error) : undefined, - all_rules: data, - rules_map: new Map(data.map((rule) => [rule.metadata.id, rule])), - rules_page: page, - total: data?.length || 0, - }; -}; - -const MAX_ITEMS_PER_PAGE = 10000; +import { RulesProvider } from './rules_context'; export const RulesContainer = () => { const params = useParams(); const history = useHistory(); - const [enabledDisabledItemsFilter, setEnabledDisabledItemsFilter] = useState('no-filter'); - const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_RULES_KEY); const navToRuleFlyout = (ruleId: string) => { history.push( @@ -91,191 +39,16 @@ export const RulesContainer = () => { ); }; - // We need to make this call without filters. this way the section list is always full - const allRules = useFindCspBenchmarkRule( - { - page: 1, - perPage: MAX_ITEMS_PER_PAGE, - sortField: 'metadata.benchmark.rule_number', - sortOrder: 'asc', - }, - params.benchmarkId, - params.benchmarkVersion - ); - - const [rulesQuery, setRulesQuery] = useState({ - section: undefined, - ruleNumber: undefined, - search: '', - page: 0, - perPage: pageSize || 10, - sortField: 'metadata.benchmark.rule_number', - sortOrder: 'asc', - }); - - // This useEffect is in charge of auto paginating to the correct page of a rule from the url params - useEffect(() => { - const getPageByRuleId = () => { - if (params.ruleId && allRules.data?.items) { - const ruleIndex = allRules.data.items.findIndex( - (rule) => rule.metadata.id === params.ruleId - ); - - if (ruleIndex !== -1) { - // Calculate the page based on the rule index and page size - const rulePage = Math.floor(ruleIndex / pageSize); - return rulePage; - } - } - return 0; - }; - - setRulesQuery({ - ...rulesQuery, - page: getPageByRuleId(), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [allRules.data?.items]); - - const { data, status, error } = useFindCspBenchmarkRule( - { - section: rulesQuery.section, - ruleNumber: rulesQuery.ruleNumber, - search: rulesQuery.search, - page: 1, - perPage: MAX_ITEMS_PER_PAGE, - sortField: 'metadata.benchmark.rule_number', - sortOrder: rulesQuery.sortOrder, - }, - params.benchmarkId, - params.benchmarkVersion - ); - - const rulesStates = useCspGetRulesStates(); - const arrayRulesStates: RuleStateAttributes[] = Object.values(rulesStates.data || {}); - - const rulesWithStates: CspBenchmarkRulesWithStates[] = useMemo(() => { - if (!data) return []; - - return data.items - .filter((rule: CspBenchmarkRule) => rule.metadata.benchmark.rule_number !== undefined) - .map((rule: CspBenchmarkRule) => { - const rulesKey = buildRuleKey( - rule.metadata.benchmark.id, - rule.metadata.benchmark.version, - /* Rule number always exists* from 8.7 */ - rule.metadata.benchmark.rule_number! - ); - - const match = rulesStates?.data?.[rulesKey]; - const rulesState = match?.muted ? 'muted' : 'unmuted'; - - return { ...rule, state: rulesState || 'unmuted' }; - }); - }, [data, rulesStates?.data]); - - const mutedRulesCount = rulesWithStates.filter((rule) => rule.state === 'muted').length; - - const filteredRulesWithStates: CspBenchmarkRulesWithStates[] = useMemo(() => { - if (enabledDisabledItemsFilter === 'disabled') - return rulesWithStates?.filter((rule) => rule?.state === 'muted'); - else if (enabledDisabledItemsFilter === 'enabled') - return rulesWithStates?.filter((rule) => rule?.state === 'unmuted'); - else return rulesWithStates; - }, [rulesWithStates, enabledDisabledItemsFilter]); - - const sectionList = useMemo( - () => allRules.data?.items.map((rule) => rule.metadata.section), - [allRules.data] - ); - - const ruleNumberList = useMemo( - () => allRules.data?.items.map((rule) => rule.metadata.benchmark.rule_number || ''), - [allRules.data] - ); - - const cleanedSectionList = [...new Set(sectionList)].sort((a, b) => { - return a.localeCompare(b, 'en', { sensitivity: 'base' }); - }); - - const cleanedRuleNumberList = [...new Set(ruleNumberList)].sort((a, b) => - semVerCompare(semVerCoerce(a) ?? '', semVerCoerce(b) ?? '') - ); - - const rulesPageData = useMemo( - () => getRulesPageData(filteredRulesWithStates, status, error, rulesQuery), - [filteredRulesWithStates, status, error, rulesQuery] - ); - - const [selectedRules, setSelectedRules] = useState([]); - - const setSelectAllRules = () => { - setSelectedRules(rulesPageData.all_rules); - }; - - const rulesFlyoutData: CspBenchmarkRulesWithStates = { - ...{ - state: - arrayRulesStates.find((filteredRuleState) => filteredRuleState.rule_id === params.ruleId) - ?.muted === true - ? 'muted' - : 'unmuted', - }, - ...{ - metadata: allRules.data?.items.find((rule) => rule.metadata.id === params.ruleId)?.metadata!, - }, - }; - return (
- - - - setRulesQuery((currentQuery) => ({ ...currentQuery, section: value })) - } - onRuleNumberChange={(value) => - setRulesQuery((currentQuery) => ({ ...currentQuery, ruleNumber: value })) - } - sectionSelectOptions={cleanedSectionList} - ruleNumberSelectOptions={cleanedRuleNumberList} - search={(value) => setRulesQuery((currentQuery) => ({ ...currentQuery, search: value }))} - searchValue={rulesQuery.search || ''} - totalRulesCount={rulesPageData.all_rules.length} - pageSize={rulesPageData.rules_page.length} - isSearching={status === 'loading'} - selectedRules={selectedRules} - setEnabledDisabledItemsFilter={setEnabledDisabledItemsFilter} - enabledDisabledItemsFilterState={enabledDisabledItemsFilter} - setSelectAllRules={setSelectAllRules} - setSelectedRules={setSelectedRules} - /> - - - setRulesQuery((currentQuery) => ({ ...currentQuery, sortOrder: value })) - } - rules_page={rulesPageData.rules_page} - total={rulesPageData.total} - error={rulesPageData.error} - loading={rulesPageData.loading} - perPage={pageSize || rulesQuery.perPage} - page={rulesQuery.page} - setPagination={(paginationQuery) => { - setPageSize(paginationQuery.perPage); - setRulesQuery((currentQuery) => ({ ...currentQuery, ...paginationQuery })); - }} - selectedRuleId={params.ruleId} - onRuleClick={navToRuleFlyout} - selectedRules={selectedRules} - setSelectedRules={setSelectedRules} - /> - {params.ruleId && rulesFlyoutData.metadata && ( - - )} + + + + + + + +
); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx new file mode 100644 index 0000000000000..f6e28a38611e8 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_context.tsx @@ -0,0 +1,317 @@ +/* + * 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, { createContext, useContext, useState, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; + +import { + CspBenchmarkRule, + FindCspBenchmarkRuleResponse, + PageUrlParams, + RuleStateAttributes, +} from '@kbn/cloud-security-posture-common/schema/rules/latest'; +import { extractErrorMessage } from '@kbn/cloud-security-posture-common'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { + CHANGE_MULTIPLE_RULE_STATE, + CHANGE_RULE_STATE, + uiMetricService, +} from '@kbn/cloud-security-posture-common/utils/ui_metrics'; +import { buildRuleKey } from '../../../common/utils/rules_states'; +import { useFindCspBenchmarkRule } from './use_csp_benchmark_rules'; +import { + RuleStateAttributesWithoutStates, + useChangeCspRuleState, +} from './use_change_csp_rule_state'; +import { usePageSize } from '../../common/hooks/use_page_size'; +import { LOCAL_STORAGE_PAGE_SIZE_RULES_KEY } from '../../common/constants'; +import { useCspGetRulesStates } from './use_csp_rules_state'; + +export interface CspBenchmarkRulesWithStates { + metadata: CspBenchmarkRule['metadata']; + state: 'muted' | 'unmuted'; +} + +// interface RulesPageData { +// rules_page: CspBenchmarkRulesWithStates[]; +// all_rules: CspBenchmarkRulesWithStates[]; +// rules_map: Map; +// total: number; +// error?: string; +// loading: boolean; +// } + +interface RulesProviderProps { + children: React.ReactNode; +} +interface RulesContextValue { + section: string[] | undefined; + setSection: (section: string[] | undefined) => void; + page: number; + setPage: (page: number) => void; + pageSize: number; + setPageSize: (pageSize: number) => void; + sortField: string; + setSortField: (sortField: string) => void; + sortOrder: 'asc' | 'desc'; + setSortOrder: (sortOrder: 'asc' | 'desc') => void; + ruleNumber: string[] | undefined; + setRuleNumber: (ruleNumber: string[] | undefined) => void; + search: string; + setSearch: (search: string) => void; + // rulesPageData: RulesPageData; + loading: boolean; + error?: string; + total: number; + rules: CspBenchmarkRulesWithStates[]; + rulesShown: number; + + // setRulesQuery: (query: Partial) => void; + + sectionList: string[] | undefined; + ruleNumberSelectOptions: string[]; + sectionSelectOptions: string[]; + selectedRules: CspBenchmarkRulesWithStates[]; + setSelectedRules: (rules: CspBenchmarkRulesWithStates[]) => void; + setSelectAllRules: () => void; + setEnabledDisabledItemsFilter: (filter: string) => void; + toggleRuleState: (rule: CspBenchmarkRulesWithStates) => void; + toggleSelectedRulesStates: (state: 'mute' | 'unmute') => void; + enabledDisabledItemsFilter: string; + mutedRulesCount?: number; + rulesFlyoutData: CspBenchmarkRulesWithStates | undefined; +} + +const RulesContext = createContext(undefined); +const MAX_ITEMS_PER_PAGE = 10000; + +export function useRules() { + const context = useContext(RulesContext); + if (context === undefined) { + throw new Error('useRules must be used within a RulesProvider'); + } + return context; +} + +export function RulesProvider({ children }: RulesProviderProps) { + const params = useParams(); + const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_RULES_KEY); + const [page, setPage] = useState(0); + const [search, setSearch] = useState(''); + const [sortField, setSortField] = useState('metadata.benchmark.rule_number'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); + const [section, setSection] = useState(undefined); + const [ruleNumber, setRuleNumber] = useState(undefined); + const { mutate: mutateRuleState } = useChangeCspRuleState(); + const [selectedRules, setSelectedRules] = useState([]); + const [enabledDisabledItemsFilter, setEnabledDisabledItemsFilter] = useState('no-filter'); + + const allRules = useFindCspBenchmarkRule( + { + page: 1, + perPage: MAX_ITEMS_PER_PAGE, + sortField: 'metadata.benchmark.rule_number', + sortOrder: 'asc', + }, + params.benchmarkId, + params.benchmarkVersion + ); + + const sectionList = allRules?.data?.items.map((rule) => rule.metadata.section) || []; + + const ruleNumberSelectOptions = + allRules?.data?.items.map((rule) => rule.metadata.benchmark.rule_number || '') || []; + + const sectionSelectOptions = [...new Set(sectionList)].sort((a, b) => { + return a.localeCompare(b, 'en', { sensitivity: 'base' }); + }); + + const { data, status, error } = useFindCspBenchmarkRule( + { + section, + ruleNumber, + search, + page: 1, + perPage: MAX_ITEMS_PER_PAGE, + sortField: 'metadata.benchmark.rule_number', + sortOrder, + }, + params.benchmarkId, + params.benchmarkVersion + ); + + const rulesStates = useCspGetRulesStates(); + + const rulesWithStates = getRulesWithStates(data, rulesStates); + + const mutedRulesCount = rulesWithStates.filter((rule) => rule.state === 'muted').length; + + const filteredRulesWithStates = getFilteredRulesWithStates( + rulesWithStates, + enabledDisabledItemsFilter + ); + + const rules = + filteredRulesWithStates.length > pageSize + ? filteredRulesWithStates.slice(page * pageSize, (page + 1) * pageSize) + : filteredRulesWithStates; + + const rulesShown = rules.length; + const total = filteredRulesWithStates.length; + const loading = status === 'loading'; + + const toggleRuleState = (rule: CspBenchmarkRulesWithStates) => { + if (rule.metadata.benchmark.rule_number) { + uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, CHANGE_RULE_STATE); + const nextRuleStates = rule.state === 'muted' ? 'unmute' : 'mute'; + const rulesObjectRequest: RuleStateAttributesWithoutStates = { + benchmark_id: rule.metadata.benchmark.id, + benchmark_version: rule.metadata.benchmark.version, + rule_number: rule.metadata.benchmark.rule_number, + rule_id: rule.metadata.id, + }; + mutateRuleState({ + newState: nextRuleStates, + ruleIds: [rulesObjectRequest], + }); + } + }; + + const toggleSelectedRulesStates = (state: 'mute' | 'unmute') => { + const bulkSelectedRules: RuleStateAttributesWithoutStates[] = selectedRules.map( + (e: CspBenchmarkRulesWithStates) => ({ + benchmark_id: e?.metadata.benchmark.id, + benchmark_version: e?.metadata.benchmark.version, + rule_number: e?.metadata.benchmark.rule_number!, + rule_id: e?.metadata.id, + }) + ); + // Only do the API Call IF there are no undefined value for rule number in the selected rules + if (!bulkSelectedRules.some((rule) => rule.rule_number === undefined)) { + uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, CHANGE_MULTIPLE_RULE_STATE); + mutateRuleState({ + newState: state, + ruleIds: bulkSelectedRules, + }); + } + setSelectedRules([]); + }; + + const setSelectAllRules = () => { + setSelectedRules(filteredRulesWithStates); + }; + + const arrayRulesStates: RuleStateAttributes[] = Object.values(rulesStates.data || {}); + const rulesFlyoutData: CspBenchmarkRulesWithStates | undefined = + !arrayRulesStates || !params.ruleId + ? undefined + : { + ...{ + state: + arrayRulesStates.find( + (filteredRuleState) => filteredRuleState.rule_id === params.ruleId + )?.muted === true + ? 'muted' + : 'unmuted', + }, + ...{ + metadata: allRules?.data?.items.find((rule) => rule.metadata.id === params.ruleId) + ?.metadata!, + }, + }; + + // This useEffect is in charge of auto paginating to the correct page of a rule from the url params + useEffect(() => { + const getPageByRuleId = () => { + if (params.ruleId && allRules?.data?.items) { + const ruleIndex = allRules?.data?.items.findIndex( + (rule) => rule.metadata.id === params.ruleId + ); + + if (ruleIndex !== -1) { + // Calculate the page based on the rule index and page size + const rulePage = Math.floor(ruleIndex / pageSize); + return rulePage; + } + } + return 0; + }; + + setPage(getPageByRuleId()); + }, [allRules?.data?.items, params.ruleId, pageSize]); + + const contextValue = { + section, + setSection, + page, + pageSize, + setPageSize, + sortField, + setSortField, + sortOrder, + setSortOrder, + ruleNumber, + setRuleNumber, + search, + setSearch, + loading, + error: error ? extractErrorMessage(error) : undefined, + total, + rules, + rulesShown, + setPage, + sectionList, + ruleNumberSelectOptions, + sectionSelectOptions, + selectedRules, + setSelectedRules, + setSelectAllRules, + setEnabledDisabledItemsFilter, + toggleRuleState, + toggleSelectedRulesStates, + enabledDisabledItemsFilter, + mutedRulesCount, + rulesFlyoutData, + }; + + return {children}; +} + +const getFilteredRulesWithStates = ( + rulesWithStates: CspBenchmarkRulesWithStates[], + enabledDisabledItemsFilter: string +) => { + let rulesWithStatesFiltered = rulesWithStates; + if (enabledDisabledItemsFilter === 'disabled') + rulesWithStatesFiltered = rulesWithStates?.filter((rule) => rule?.state === 'muted'); + else if (enabledDisabledItemsFilter === 'enabled') + rulesWithStatesFiltered = rulesWithStates?.filter((rule) => rule?.state === 'unmuted'); + + return rulesWithStatesFiltered; +}; + +const getRulesWithStates = ( + data: FindCspBenchmarkRuleResponse | undefined, + rulesStates: ReturnType +): CspBenchmarkRulesWithStates[] => { + if (!data) return []; + + return data.items + .filter((rule: CspBenchmarkRule) => rule.metadata.benchmark.rule_number !== undefined) + .map((rule: CspBenchmarkRule) => { + const rulesKey = buildRuleKey( + rule.metadata.benchmark.id, + rule.metadata.benchmark.version, + /* Rule number always exists* from 8.7 */ + rule.metadata.benchmark.rule_number! + ); + + const match = rulesStates?.data?.[rulesKey]; + const rulesState = match?.muted ? 'muted' : 'unmuted'; + + return { ...rule, state: rulesState || 'unmuted' }; + }); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_counters.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_counters.tsx index 15dca8e9b76ff..0509511162148 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_counters.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_counters.tsx @@ -29,6 +29,7 @@ import { RULE_FAILED, RULE_PASSED } from '../../../common/constants'; import { useCspBenchmarkIntegrationsV2 } from '../benchmarks/use_csp_benchmark_integrations'; import { CspCounterCard } from '../../components/csp_counter_card'; import { useKibana } from '../../common/hooks/use_kibana'; +import { useRules } from './rules_context'; const EvaluationPieChart = ({ failed, passed }: { failed: number; passed: number }) => { const { @@ -84,18 +85,13 @@ const EvaluationPieChart = ({ failed, passed }: { failed: number; passed: number ); }; -export const RulesCounters = ({ - mutedRulesCount, - setEnabledDisabledItemsFilter, -}: { - mutedRulesCount: number; - setEnabledDisabledItemsFilter: (filterState: string) => void; -}) => { +export const RulesCounters = () => { const { http } = useKibana().services; const { getBenchmarkDynamicValues } = useBenchmarkDynamicValues(); const rulesPageParams = useParams<{ benchmarkId: string; benchmarkVersion: string }>(); const getBenchmarks = useCspBenchmarkIntegrationsV2(); const navToFindings = useNavigateFindings(); + const { setEnabledDisabledItemsFilter, mutedRulesCount } = useRules(); const benchmarkRulesStats = getBenchmarks.data?.items.find( (benchmark) => diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx index 7447d82d251ee..d04ce342171d7 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_flyout.tsx @@ -27,16 +27,14 @@ import type { CspBenchmarkRuleMetadata } from '@kbn/cloud-security-posture-commo import { getRuleList } from '../configurations/findings_flyout/rule_tab'; import { getRemediationList } from '../configurations/findings_flyout/overview_tab'; import * as TEST_SUBJECTS from './test_subjects'; -import { useChangeCspRuleState } from './use_change_csp_rule_state'; -import { CspBenchmarkRulesWithStates } from './rules_container'; import { TakeAction } from '../../components/take_action'; import { createDetectionRuleFromBenchmarkRule } from '../configurations/utils/create_detection_rule_from_benchmark'; +import { CspBenchmarkRulesWithStates, useRules } from './rules_context'; export const RULES_FLYOUT_SWITCH_BUTTON = 'rule-flyout-switch-button'; interface RuleFlyoutProps { onClose(): void; - rule: CspBenchmarkRulesWithStates; } const tabs = [ @@ -58,27 +56,15 @@ const tabs = [ type RuleTab = (typeof tabs)[number]['id']; -export const RuleFlyout = ({ onClose, rule }: RuleFlyoutProps) => { +export const RuleFlyout = ({ onClose }: RuleFlyoutProps) => { const [tab, setTab] = useState('overview'); + const { toggleRuleState, rulesFlyoutData: rule } = useRules(); - const isRuleMuted = rule?.state === 'muted'; - const { mutate: mutateRuleState } = useChangeCspRuleState(); + if (!rule) { + return null; + } - const switchRuleStates = async () => { - if (rule.metadata.benchmark.rule_number) { - const rulesObjectRequest = { - benchmark_id: rule.metadata.benchmark.id, - benchmark_version: rule.metadata.benchmark.version, - rule_number: rule.metadata.benchmark.rule_number, - rule_id: rule.metadata.id, - }; - const nextRuleStates = isRuleMuted ? 'unmute' : 'mute'; - mutateRuleState({ - newState: nextRuleStates, - ruleIds: [rulesObjectRequest], - }); - } - }; + const isRuleMuted = rule?.state === 'muted'; const createMisconfigurationRuleFn = async (http: HttpSetup) => await createDetectionRuleFromBenchmarkRule(http, rule.metadata); @@ -107,13 +93,7 @@ export const RuleFlyout = ({ onClose, rule }: RuleFlyoutProps) => { - {tab === 'overview' && ( - - )} + {tab === 'overview' && } {tab === 'remediation' && ( )} @@ -123,13 +103,13 @@ export const RuleFlyout = ({ onClose, rule }: RuleFlyoutProps) => { {isRuleMuted ? ( toggleRuleState(rule)} createRuleFn={createMisconfigurationRuleFn} isCreateDetectionRuleDisabled={true} /> ) : ( toggleRuleState(rule)} createRuleFn={createMisconfigurationRuleFn} isCreateDetectionRuleDisabled={false} /> @@ -144,22 +124,26 @@ export const RuleFlyout = ({ onClose, rule }: RuleFlyoutProps) => { const RuleOverviewTab = ({ rule, ruleData, - switchRuleStates, }: { rule: CspBenchmarkRuleMetadata; ruleData: CspBenchmarkRulesWithStates; - switchRuleStates: () => Promise; -}) => ( - - - - - -); +}) => { + const { toggleRuleState } = useRules(); + return ( + + + toggleRuleState(ruleData)), + ...getRuleList(rule, ruleData.state), + ]} + /> + + + ); +}; -const ruleState = (rule: CspBenchmarkRulesWithStates, switchRuleStates: () => Promise) => [ +const ruleState = (rule: CspBenchmarkRulesWithStates, switchRuleStates: () => void) => [ { title: ( 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 a30e009c85198..2cbe5c2d4732b 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 @@ -19,36 +19,22 @@ import { EuiTableSortingType, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { METRIC_TYPE } from '@kbn/analytics'; -import { - CHANGE_RULE_STATE, - uiMetricService, -} from '@kbn/cloud-security-posture-common/utils/ui_metrics'; import { uniqBy } from 'lodash'; import { ColumnNameWithTooltip } from '../../components/column_name_with_tooltip'; -import type { CspBenchmarkRulesWithStates, RulesState } from './rules_container'; import * as TEST_SUBJECTS from './test_subjects'; -import { useChangeCspRuleState } from './use_change_csp_rule_state'; +import { CspBenchmarkRulesWithStates, useRules } from './rules_context'; export const RULES_ROWS_ENABLE_SWITCH_BUTTON = 'rules-row-enable-switch-button'; export const RULES_ROW_SELECT_ALL_CURRENT_PAGE = 'cloud-security-fields-selector-item-all'; -type RulesTableProps = Pick< - RulesState, - 'loading' | 'error' | 'rules_page' | 'total' | 'perPage' | 'page' -> & { - setPagination(pagination: Pick): void; +interface RulesTableProps { onRuleClick: (ruleID: string) => void; selectedRuleId?: string; +} + +type GetColumnProps = Pick & { selectedRules: CspBenchmarkRulesWithStates[]; setSelectedRules: (rules: CspBenchmarkRulesWithStates[]) => void; - onSortChange: (value: 'asc' | 'desc') => void; -}; - -type GetColumnProps = Pick< - RulesTableProps, - 'onRuleClick' | 'selectedRules' | 'setSelectedRules' -> & { items: CspBenchmarkRulesWithStates[]; setIsAllRulesSelectedThisPage: (isAllRulesSelected: boolean) => void; isAllRulesSelectedThisPage: boolean; @@ -58,43 +44,44 @@ type GetColumnProps = Pick< ) => boolean; }; -export const RulesTable = ({ - setPagination, - perPage: pageSize, - rules_page: items, - page, - total, - loading, - error, - selectedRuleId, - selectedRules, - setSelectedRules, - onRuleClick, - onSortChange, -}: RulesTableProps) => { +export const RulesTable = ({ selectedRuleId, onRuleClick }: RulesTableProps) => { const { euiTheme } = useEuiTheme(); + const { + page, + setPage, + setSelectedRules, + selectedRules, + rules: items, + total, + error, + loading, + pageSize, + setPageSize, + sortOrder, + setSortOrder, + } = useRules(); + const euiPagination: EuiBasicTableProps['pagination'] = { pageIndex: page, pageSize, totalItemCount: total, pageSizeOptions: [10, 25, 100], }; - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); + const sorting: EuiTableSortingType = { sort: { field: 'metadata.benchmark.rule_number' as keyof CspBenchmarkRulesWithStates, - direction: sortDirection, + direction: sortOrder, }, }; - const onTableChange = ({ - page: pagination, - sort: sortOrder, - }: Criteria) => { + const onTableChange = ({ page: pagination, sort }: Criteria) => { if (!pagination) return; - if (pagination) setPagination({ page: pagination.index, perPage: pagination.size }); - if (sortOrder) { - setSortDirection(sortOrder.direction); - onSortChange(sortOrder.direction); + if (pagination && (pagination.index !== page || pagination.size !== pageSize)) { + setPageSize(pagination.size); + setPage(pagination.index); + } + if (sort && sort.direction !== sortOrder) { + setSortOrder(sort.direction); } }; @@ -272,34 +259,16 @@ const getColumns = ({ ]; const RuleStateSwitch = ({ rule }: { rule: CspBenchmarkRulesWithStates }) => { + const { toggleRuleState } = useRules(); const isRuleMuted = rule?.state === 'muted'; - const nextRuleState = isRuleMuted ? 'unmute' : 'mute'; - const { mutate: mutateRulesStates } = useChangeCspRuleState(); - - const rulesObjectRequest = { - benchmark_id: rule?.metadata.benchmark.id, - benchmark_version: rule?.metadata.benchmark.version, - /* Rule number always exists from 8.7 */ - rule_number: rule?.metadata.benchmark.rule_number!, - rule_id: rule?.metadata.id, - }; - const changeCspRuleStateFn = async () => { - if (rule?.metadata.benchmark.rule_number) { - uiMetricService.trackUiMetric(METRIC_TYPE.COUNT, CHANGE_RULE_STATE); - mutateRulesStates({ - newState: nextRuleState, - ruleIds: [rulesObjectRequest], - }); - } - }; return ( toggleRuleState(rule)} data-test-subj={RULES_ROWS_ENABLE_SWITCH_BUTTON} label="" compressed={true} 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 a7c69326d3964..b7fca82c235b5 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 @@ -23,12 +23,8 @@ import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; -import { - RuleStateAttributesWithoutStates, - useChangeCspRuleState, -} from './use_change_csp_rule_state'; -import { CspBenchmarkRulesWithStates } from './rules_container'; import { MultiSelectFilter } from '../../common/component/multi_select_filter'; +import { useRules } from './rules_context'; export const RULES_BULK_ACTION_BUTTON = 'bulk-action-button'; export const RULES_BULK_ACTION_OPTION_ENABLE = 'bulk-action-option-enable'; @@ -38,49 +34,17 @@ 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'; -interface RulesTableToolbarProps { - search: (value: string) => void; - onSectionChange: (value: string[] | undefined) => void; - onRuleNumberChange: (value: string[] | undefined) => void; - sectionSelectOptions: string[]; - ruleNumberSelectOptions: string[]; - totalRulesCount: number; - searchValue: string; - isSearching: boolean; - pageSize: number; - selectedRules: CspBenchmarkRulesWithStates[]; - setEnabledDisabledItemsFilter: (filterState: string) => void; - enabledDisabledItemsFilterState: string; - setSelectAllRules: () => void; - setSelectedRules: (rules: CspBenchmarkRulesWithStates[]) => void; -} - -interface RuleTableCount { - pageSize: number; - total: number; - selectedRules: CspBenchmarkRulesWithStates[]; - setSelectAllRules: () => void; - setSelectedRules: (rules: CspBenchmarkRulesWithStates[]) => void; -} - -export const RulesTableHeader = ({ - search, - searchValue, - isSearching, - totalRulesCount, - pageSize, - onSectionChange, - onRuleNumberChange, - sectionSelectOptions, - ruleNumberSelectOptions, - selectedRules, - setEnabledDisabledItemsFilter, - enabledDisabledItemsFilterState, - setSelectAllRules, - setSelectedRules, -}: RulesTableToolbarProps) => { - const [selectedSection, setSelectedSection] = useState([]); - const [selectedRuleNumber, setSelectedRuleNumber] = useState([]); +export const RulesTableHeader = () => { + const { + section, + setSection, + ruleNumber, + setRuleNumber, + sectionSelectOptions, + ruleNumberSelectOptions, + setEnabledDisabledItemsFilter, + enabledDisabledItemsFilter, + } = useRules(); const sectionOptions = sectionSelectOptions.map((option) => ({ key: option, label: option, @@ -91,12 +55,12 @@ export const RulesTableHeader = ({ })); const toggleEnabledRulesFilter = () => { - if (enabledDisabledItemsFilterState === 'enabled') setEnabledDisabledItemsFilter('no-filter'); + if (enabledDisabledItemsFilter === 'enabled') setEnabledDisabledItemsFilter('no-filter'); else setEnabledDisabledItemsFilter('enabled'); }; const toggleDisabledRulesFilter = () => { - if (enabledDisabledItemsFilterState === 'disabled') setEnabledDisabledItemsFilter('no-filter'); + if (enabledDisabledItemsFilter === 'disabled') setEnabledDisabledItemsFilter('no-filter'); else setEnabledDisabledItemsFilter('disabled'); }; @@ -104,7 +68,7 @@ export const RulesTableHeader = ({ - + @@ -121,14 +85,15 @@ export const RulesTableHeader = ({ } )} id={'cis-section-multi-select-filter'} - onChange={(section) => { - setSelectedSection([...section?.selectedOptionKeys]); - onSectionChange( - section?.selectedOptionKeys ? section?.selectedOptionKeys : undefined + onChange={(changedSections) => { + setSection( + changedSections?.selectedOptionKeys + ? changedSections?.selectedOptionKeys + : undefined ); }} options={sectionOptions} - selectedOptionKeys={selectedSection} + selectedOptionKeys={section} /> { - setSelectedRuleNumber([...ruleNumber?.selectedOptionKeys]); - onRuleNumberChange( - ruleNumber?.selectedOptionKeys ? ruleNumber?.selectedOptionKeys : undefined + onChange={(changedRuleNumbers) => { + setRuleNumber( + changedRuleNumbers?.selectedOptionKeys + ? changedRuleNumbers?.selectedOptionKeys + : undefined ); }} options={ruleNumberOptions} - selectedOptionKeys={selectedRuleNumber} + selectedOptionKeys={ruleNumber} /> @@ -172,7 +138,7 @@ export const RulesTableHeader = ({ /> @@ -187,13 +153,7 @@ export const RulesTableHeader = ({ - + ); @@ -201,20 +161,17 @@ export const RulesTableHeader = ({ const SEARCH_DEBOUNCE_MS = 300; -const SearchField = ({ - search, - isSearching, - searchValue, -}: Pick) => { - const [localValue, setLocalValue] = useState(searchValue); +const SearchField = () => { + const { search, setSearch, loading } = useRules(); + const [localValue, setLocalValue] = useState(search); - useDebounce(() => search(localValue), SEARCH_DEBOUNCE_MS, [localValue]); + useDebounce(() => setSearch(localValue), SEARCH_DEBOUNCE_MS, [localValue]); return (
{ +const CurrentPageOfTotal = () => { + const { + selectedRules, + setSelectedRules, + rulesShown, + total, + setSelectAllRules, + toggleSelectedRulesStates, + } = useRules(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const onPopoverClick = () => { setIsPopoverOpen((e) => !e); }; - const { mutate: mutateRulesStates } = useChangeCspRuleState(); - - const changeCspRuleState = (state: 'mute' | 'unmute') => { - const bulkSelectedRules: RuleStateAttributesWithoutStates[] = selectedRules.map( - (e: CspBenchmarkRulesWithStates) => ({ - benchmark_id: e?.metadata.benchmark.id, - benchmark_version: e?.metadata.benchmark.version, - rule_number: e?.metadata.benchmark.rule_number!, - rule_id: e?.metadata.id, - }) - ); - // Only do the API Call IF there are no undefined value for rule number in the selected rules - if (!bulkSelectedRules.some((rule) => rule.rule_number === undefined)) { - mutateRulesStates({ - newState: state, - ruleIds: bulkSelectedRules, - }); - setIsPopoverOpen(false); - } - setSelectedRules([]); - }; - const changeCspRuleStateMute = () => { - changeCspRuleState('mute'); + toggleSelectedRulesStates('mute'); }; const changeCspRuleStateUnmute = () => { - changeCspRuleState('unmute'); + toggleSelectedRulesStates('unmute'); }; const areAllSelectedRulesMuted = selectedRules.every((rule) => rule?.state === 'muted'); @@ -312,9 +249,9 @@ const CurrentPageOfTotal = ({