diff --git a/x-pack/plugins/cloud_security_posture/common/runtime_mappings/get_belongs_to_runtime_mapping.ts b/x-pack/plugins/cloud_security_posture/common/runtime_mappings/get_belongs_to_runtime_mapping.ts index 3ad655738e24e..ac978f3b220e4 100644 --- a/x-pack/plugins/cloud_security_posture/common/runtime_mappings/get_belongs_to_runtime_mapping.ts +++ b/x-pack/plugins/cloud_security_posture/common/runtime_mappings/get_belongs_to_runtime_mapping.ts @@ -18,8 +18,8 @@ export const getBelongsToRuntimeMapping = (): MappingRuntimeFields => ({ source: ` if (!doc.containsKey('rule.benchmark.posture_type')) { - def identifier = doc["cluster_id"].value; - emit(identifier); + def belongs_to = doc["cluster_id"].value; + emit(belongs_to); return } else @@ -29,21 +29,21 @@ export const getBelongsToRuntimeMapping = (): MappingRuntimeFields => ({ def policy_template_type = doc["rule.benchmark.posture_type"].value; if (policy_template_type == "cspm") { - def identifier = doc["cloud.account.name"].value; - emit(identifier); + def belongs_to = doc["cloud.account.name"].value; + emit(belongs_to); return } if (policy_template_type == "kspm") { - def identifier = doc["cluster_id"].value; - emit(identifier); + def belongs_to = doc["cluster_id"].value; + emit(belongs_to); return } } - def identifier = doc["cluster_id"].value; - emit(identifier); + def belongs_to = doc["cluster_id"].value; + emit(belongs_to); return } `, diff --git a/x-pack/plugins/cloud_security_posture/common/runtime_mappings/get_safe_posture_type_runtime_mapping.ts b/x-pack/plugins/cloud_security_posture/common/runtime_mappings/get_safe_posture_type_runtime_mapping.ts new file mode 100644 index 0000000000000..2fb06e6ea550c --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/common/runtime_mappings/get_safe_posture_type_runtime_mapping.ts @@ -0,0 +1,34 @@ +/* + * 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 { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; + +/** + * Creates the `safe_posture_type` runtime field with the value of either + * `kspm` or `cspm` based on the value of `rule.benchmark.posture_type` + */ +export const getSafePostureTypeRuntimeMapping = (): MappingRuntimeFields => ({ + safe_posture_type: { + type: 'keyword', + script: { + source: ` + if (!doc.containsKey('rule.benchmark.posture_type')) + { + def safe_posture_type = 'kspm'; + emit(safe_posture_type); + return + } + else + { + def safe_posture_type = doc["rule.benchmark.posture_type"].value; + emit(safe_posture_type); + return + } + `, + }, + }, +}); diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.test.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.test.ts index 616fa533e7c52..b18a5833a093f 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.test.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.test.ts @@ -35,37 +35,52 @@ describe('useNavigateFindings', () => { const push = jest.fn(); (useHistory as jest.Mock).mockReturnValueOnce({ push }); - const filter = { foo: 1 }; + const { result } = renderHook(() => useNavigateFindings()); + + act(() => { + result.current({ foo: 1 }); + }); + + expect(push).toHaveBeenCalledWith({ + pathname: '/cloud_security_posture/findings/default', + search: + "cspq=(filters:!((meta:(alias:!n,disabled:!f,key:foo,negate:!f,type:phrase),query:(match_phrase:(foo:1)))),query:(language:kuery,query:''))", + }); + expect(push).toHaveBeenCalledTimes(1); + }); + + it('creates a URL to findings page with correct path and negated filter', () => { + const push = jest.fn(); + (useHistory as jest.Mock).mockReturnValueOnce({ push }); const { result } = renderHook(() => useNavigateFindings()); act(() => { - result.current({ filter }); + result.current({ foo: { value: 1, negate: true } }); }); expect(push).toHaveBeenCalledWith({ pathname: '/cloud_security_posture/findings/default', search: - "cspq=(filters:!((meta:(alias:!n,disabled:!f,key:filter,negate:!f,params:(query:(foo:1)),type:phrase),query:(match_phrase:(filter:(foo:1))))),query:(language:kuery,query:''))", + "cspq=(filters:!((meta:(alias:!n,disabled:!f,key:foo,negate:!t,type:phrase),query:(match_phrase:(foo:1)))),query:(language:kuery,query:''))", }); expect(push).toHaveBeenCalledTimes(1); }); + it('creates a URL to findings resource page with correct path and filter', () => { const push = jest.fn(); (useHistory as jest.Mock).mockReturnValueOnce({ push }); - const filter = { foo: 1 }; - const { result } = renderHook(() => useNavigateFindingsByResource()); act(() => { - result.current({ filter }); + result.current({ foo: 1 }); }); expect(push).toHaveBeenCalledWith({ pathname: '/cloud_security_posture/findings/resource', search: - "cspq=(filters:!((meta:(alias:!n,disabled:!f,key:filter,negate:!f,params:(query:(foo:1)),type:phrase),query:(match_phrase:(filter:(foo:1))))),query:(language:kuery,query:''))", + "cspq=(filters:!((meta:(alias:!n,disabled:!f,key:foo,negate:!f,type:phrase),query:(match_phrase:(foo:1)))),query:(language:kuery,query:''))", }); expect(push).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.ts index 946941b181292..08d4a169c6aba 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_navigate_findings.ts @@ -12,25 +12,45 @@ import { findingsNavigation } from '../navigation/constants'; import { encodeQuery } from '../navigation/query_utils'; import { useKibana } from './use_kibana'; -const createFilter = (key: string, value: string, negate = false): Filter => ({ - meta: { - alias: null, - negate, - disabled: false, - type: 'phrase', - key, - params: { query: value }, - }, - query: { match_phrase: { [key]: value } }, -}); +interface NegatedValue { + value: string | number; + negate: boolean; +} + +type FilterValue = string | number | NegatedValue; + +export type NavFilter = Record; + +const createFilter = (key: string, filterValue: FilterValue): Filter => { + let negate = false; + let value = filterValue; + if (typeof filterValue === 'object') { + negate = filterValue.negate; + value = filterValue.value; + } + + return { + meta: { + alias: null, + negate, + disabled: false, + type: 'phrase', + key, + }, + query: { match_phrase: { [key]: value } }, + }; +}; const useNavigate = (pathname: string) => { const history = useHistory(); const { services } = useKibana(); return useCallback( - (filterParams: Record = {}) => { - const filters = Object.entries(filterParams).map(([key, value]) => createFilter(key, value)); + (filterParams: NavFilter = {}) => { + const filters = Object.entries(filterParams).map(([key, filterValue]) => + createFilter(key, filterValue) + ); + history.push({ pathname, search: encodeQuery({ diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx index 44d2795337bf4..036bf86003d2e 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx @@ -26,7 +26,7 @@ import { KSPM_POLICY_TEMPLATE, RULE_FAILED, } from '../../../../common/constants'; -import { useNavigateFindings } from '../../../common/hooks/use_navigate_findings'; +import { NavFilter, useNavigateFindings } from '../../../common/hooks/use_navigate_findings'; import { ClusterDetailsBox } from './cluster_details_box'; import { dashboardColumnsGrow, getPolicyTemplateQuery } from './summary_section'; import { @@ -36,15 +36,12 @@ import { const CLUSTER_DEFAULT_SORT_ORDER = 'asc'; -export const getClusterIdQuery = (cluster: Cluster) => { +export const getClusterIdQuery = (cluster: Cluster): NavFilter => { if (cluster.meta.benchmark.posture_type === CSPM_POLICY_TEMPLATE) { - return { 'cloud.account.name': cluster.meta.cloud?.account.name }; + // TODO: remove assertion after typing CspFinding as discriminating union + return { 'cloud.account.name': cluster.meta.cloud!.account.name }; } - if (cluster.meta.benchmark.posture_type === 'kspm') { - return { cluster_id: cluster.meta.assetIdentifierId }; - } - - return {}; + return { cluster_id: cluster.meta.assetIdentifierId }; }; export const BenchmarksSection = ({ diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cluster_details_box.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cluster_details_box.tsx index 39fb171fbb64a..7b42445d26b99 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cluster_details_box.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cluster_details_box.tsx @@ -20,7 +20,7 @@ import moment from 'moment'; import React from 'react'; import { i18n } from '@kbn/i18n'; import { getClusterIdQuery } from './benchmarks_section'; -import { INTERNAL_FEATURE_FLAGS } from '../../../../common/constants'; +import { CSPM_POLICY_TEMPLATE, INTERNAL_FEATURE_FLAGS } from '../../../../common/constants'; import { Cluster } from '../../../../common/types'; import { useNavigateFindings } from '../../../common/hooks/use_navigate_findings'; import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon'; @@ -31,13 +31,16 @@ const defaultClusterTitle = i18n.translate( ); const getClusterTitle = (cluster: Cluster) => { - if (cluster.meta.benchmark.posture_type === 'cspm') return cluster.meta.cloud?.account.name; + if (cluster.meta.benchmark.posture_type === CSPM_POLICY_TEMPLATE) { + return cluster.meta.cloud?.account.name; + } + return cluster.meta.cluster?.name; }; const getClusterId = (cluster: Cluster) => { const assetIdentifierId = cluster.meta.assetIdentifierId; - if (cluster.meta.benchmark.posture_type === 'cspm') return assetIdentifierId; + if (cluster.meta.benchmark.posture_type === CSPM_POLICY_TEMPLATE) return assetIdentifierId; return assetIdentifierId.slice(0, 6); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx index c5e71e0090668..647b022036676 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx @@ -8,6 +8,7 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFlexItemProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; import { statusColors } from '../../../common/constants'; import { DASHBOARD_COUNTER_CARDS } from '../test_subjects'; import { CspCounterCard, CspCounterCardProps } from '../../../components/csp_counter_card'; @@ -21,6 +22,7 @@ import type { } from '../../../../common/types'; import { RisksTable } from '../compliance_charts/risks_table'; import { + NavFilter, useNavigateFindings, useNavigateFindingsByResource, } from '../../../common/hooks/use_navigate_findings'; @@ -36,12 +38,12 @@ export const dashboardColumnsGrow: Record = { third: 8, }; -export const getPolicyTemplateQuery = (policyTemplate: PosturePolicyTemplate) => { - if (policyTemplate === CSPM_POLICY_TEMPLATE) +export const getPolicyTemplateQuery = (policyTemplate: PosturePolicyTemplate): NavFilter => { + if (policyTemplate === CSPM_POLICY_TEMPLATE) { return { 'rule.benchmark.posture_type': CSPM_POLICY_TEMPLATE }; - if (policyTemplate === KSPM_POLICY_TEMPLATE) - return { 'rule.benchmark.posture_type': KSPM_POLICY_TEMPLATE }; - return {}; + } + + return { 'rule.benchmark.posture_type': { value: CSPM_POLICY_TEMPLATE, negate: true } }; }; export const SummarySection = ({ @@ -131,7 +133,13 @@ export const SummarySection = ({ }); return ( - + {counters.map((counter) => ( diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts index 6789b8674499a..d242c7a587dda 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts @@ -8,6 +8,8 @@ import { transformError } from '@kbn/securitysolution-es-utils'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { schema } from '@kbn/config-schema'; +import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; +import { getSafePostureTypeRuntimeMapping } from '../../../common/runtime_mappings/get_safe_posture_type_runtime_mapping'; import type { PosturePolicyTemplate, ComplianceDashboardData } from '../../../common/types'; import { CSPM_POLICY_TEMPLATE, @@ -69,17 +71,20 @@ export const defineGetComplianceDashboardRoute = (router: CspRouter): void => const policyTemplate = request.params.policy_template as PosturePolicyTemplate; + // runtime mappings create the `safe_posture_type` field, which equals to `kspm` or `cspm` based on the value and existence of the `posture_type` field which was introduced at 8.7 + // the `query` is then being passed to our getter functions to filter per posture type even for older findings before 8.7 + const runtimeMappings: MappingRuntimeFields = getSafePostureTypeRuntimeMapping(); const query: QueryDslQueryContainer = { bool: { - filter: [{ term: { 'rule.benchmark.posture_type': policyTemplate } }], + filter: [{ term: { safe_posture_type: policyTemplate } }], }, }; const [stats, groupedFindingsEvaluation, clustersWithoutTrends, trends] = await Promise.all( [ - getStats(esClient, query, pitId), - getGroupedFindingsEvaluation(esClient, query, pitId), - getClusters(esClient, query, pitId), + getStats(esClient, query, pitId, runtimeMappings), + getGroupedFindingsEvaluation(esClient, query, pitId, runtimeMappings), + getClusters(esClient, query, pitId, runtimeMappings), getTrends(esClient, policyTemplate), ] ); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts index 736ba66a25508..750c035dc9e08 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts @@ -13,6 +13,7 @@ import type { AggregationsTopHitsAggregate, SearchHit, } from '@elastic/elasticsearch/lib/api/types'; +import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; import { CspFinding } from '../../../common/schemas/csp_finding'; import type { Cluster } from '../../../common/types'; import { @@ -40,9 +41,15 @@ interface ClustersQueryResult { export type ClusterWithoutTrend = Omit; -export const getClustersQuery = (query: QueryDslQueryContainer, pitId: string): SearchRequest => ({ +export const getClustersQuery = ( + query: QueryDslQueryContainer, + pitId: string, + runtimeMappings: MappingRuntimeFields +): SearchRequest => ({ size: 0, - runtime_mappings: getIdentifierRuntimeMapping(), + // creates the `asset_identifier` and `safe_posture_type` runtime fields, + // `safe_posture_type` is used by the `query` to filter by posture type for older findings without this field + runtime_mappings: { ...runtimeMappings, ...getIdentifierRuntimeMapping() }, query, aggs: { aggs_by_asset_identifier: { @@ -101,10 +108,11 @@ export const getClustersFromAggs = (clusters: ClusterBucket[]): ClusterWithoutTr export const getClusters = async ( esClient: ElasticsearchClient, query: QueryDslQueryContainer, - pitId: string + pitId: string, + runtimeMappings: MappingRuntimeFields ): Promise => { const queryResult = await esClient.search( - getClustersQuery(query, pitId) + getClustersQuery(query, pitId, runtimeMappings) ); const clusters = queryResult.aggregations?.aggs_by_asset_identifier.buckets; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.ts index f20b43619e914..239801350c7af 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.ts @@ -11,6 +11,7 @@ import type { QueryDslQueryContainer, SearchRequest, } from '@elastic/elasticsearch/lib/api/types'; +import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; import { calculatePostureScore } from '../../../common/utils/helpers'; import type { ComplianceDashboardData } from '../../../common/types'; import { KeyDocCount } from './compliance_dashboard'; @@ -62,8 +63,15 @@ export const failedFindingsAggQuery = { }, }; -export const getRisksEsQuery = (query: QueryDslQueryContainer, pitId: string): SearchRequest => ({ +export const getRisksEsQuery = ( + query: QueryDslQueryContainer, + pitId: string, + runtimeMappings: MappingRuntimeFields +): SearchRequest => ({ size: 0, + // creates the `safe_posture_type` runtime fields, + // `safe_posture_type` is used by the `query` to filter by posture type for older findings without this field + runtime_mappings: runtimeMappings, query, aggs: failedFindingsAggQuery, pit: { @@ -90,10 +98,11 @@ export const getFailedFindingsFromAggs = ( export const getGroupedFindingsEvaluation = async ( esClient: ElasticsearchClient, query: QueryDslQueryContainer, - pitId: string + pitId: string, + runtimeMappings: MappingRuntimeFields ): Promise => { const resourceTypesQueryResult = await esClient.search( - getRisksEsQuery(query, pitId) + getRisksEsQuery(query, pitId, runtimeMappings) ); const ruleSections = resourceTypesQueryResult.aggregations?.aggs_by_resource_type.buckets; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts index a59d7487488e0..2f0e1c1b17102 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts @@ -7,6 +7,7 @@ import { ElasticsearchClient } from '@kbn/core/server'; import type { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; import { calculatePostureScore } from '../../../common/utils/helpers'; import type { ComplianceDashboardData } from '../../../common/types'; @@ -41,10 +42,14 @@ const uniqueResourcesCountQuery = { export const getEvaluationsQuery = ( query: QueryDslQueryContainer, - pitId: string + pitId: string, + runtimeMappings: MappingRuntimeFields ): SearchRequest => ({ - query, size: 0, + // creates the `safe_posture_type` runtime fields, + // `safe_posture_type` is used by the `query` to filter by posture type for older findings without this field + runtime_mappings: runtimeMappings, + query, aggs: { ...findingsEvaluationAggsQuery, ...uniqueResourcesCountQuery, @@ -75,10 +80,11 @@ export const getStatsFromFindingsEvaluationsAggs = ( export const getStats = async ( esClient: ElasticsearchClient, query: QueryDslQueryContainer, - pitId: string + pitId: string, + runtimeMappings: MappingRuntimeFields ): Promise => { const evaluationsQueryResult = await esClient.search( - getEvaluationsQuery(query, pitId) + getEvaluationsQuery(query, pitId, runtimeMappings) ); const findingsEvaluations = evaluationsQueryResult.aggregations; diff --git a/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts b/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts index 2e8401cebd932..6f21112efcb90 100644 --- a/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts +++ b/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts @@ -14,6 +14,7 @@ import { import { SearchRequest } from '@kbn/data-plugin/common'; import { ElasticsearchClient } from '@kbn/core/server'; import type { Logger } from '@kbn/core/server'; +import { getSafePostureTypeRuntimeMapping } from '../../common/runtime_mappings/get_safe_posture_type_runtime_mapping'; import { getIdentifierRuntimeMapping } from '../../common/runtime_mappings/get_identifier_runtime_mapping'; import { FindingsStatsTaskResult, TaskHealthStatus, ScoreByPolicyTemplateBucket } from './types'; import { @@ -108,14 +109,15 @@ export function taskRunner(coreStartServices: CspServerPluginStartServices, logg const getScoreQuery = (): SearchRequest => ({ index: LATEST_FINDINGS_INDEX_DEFAULT_NS, size: 0, - runtime_mappings: getIdentifierRuntimeMapping(), + // creates the safe_posture_type and asset_identifier runtime fields + runtime_mappings: { ...getIdentifierRuntimeMapping(), ...getSafePostureTypeRuntimeMapping() }, query: { match_all: {}, }, aggs: { score_by_policy_template: { terms: { - field: 'rule.benchmark.posture_type', + field: 'safe_posture_type', }, aggs: { total_findings: {