diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cbffef0027017..431a668ebe14c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1725,6 +1725,7 @@ x-pack/plugins/osquery @elastic/security-defend-workflows /x-pack/plugins/fleet/public/components/cloud_security_posture @elastic/fleet @elastic/kibana-cloud-security-posture /x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/cloud_security_posture @elastic/fleet @elastic/kibana-cloud-security-posture /x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.* @elastic/fleet @elastic/kibana-cloud-security-posture +/x-pack/plugins/security_solution/public/cloud_security_posture @elastic/kibana-cloud-security-posture # Security Solution onboarding tour /x-pack/plugins/security_solution/public/common/components/guided_onboarding @elastic/security-threat-hunting-explore diff --git a/x-pack/packages/kbn-cloud-security-posture-common/index.ts b/x-pack/packages/kbn-cloud-security-posture-common/index.ts index 66a98d5e398c3..b5211af3342a3 100644 --- a/x-pack/packages/kbn-cloud-security-posture-common/index.ts +++ b/x-pack/packages/kbn-cloud-security-posture-common/index.ts @@ -20,4 +20,8 @@ export type { export type { CspFinding } from './types/findings'; export type { BenchmarksCisId } from './types/benchmark'; export * from './constants'; -export { extractErrorMessage, buildMutedRulesFilter } from './utils/helpers'; +export { + extractErrorMessage, + buildMutedRulesFilter, + buildEntityFlyoutPreviewQuery, +} from './utils/helpers'; diff --git a/x-pack/packages/kbn-cloud-security-posture-common/utils/helpers.test.ts b/x-pack/packages/kbn-cloud-security-posture-common/utils/helpers.test.ts index 7e5f4f1d8120a..d920a8dc25165 100644 --- a/x-pack/packages/kbn-cloud-security-posture-common/utils/helpers.test.ts +++ b/x-pack/packages/kbn-cloud-security-posture-common/utils/helpers.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { extractErrorMessage, defaultErrorMessage, buildMutedRulesFilter } from './helpers'; +import { + extractErrorMessage, + defaultErrorMessage, + buildMutedRulesFilter, + buildEntityFlyoutPreviewQuery, +} from './helpers'; const fallbackMessage = 'thisIsAFallBackMessage'; @@ -138,4 +143,43 @@ describe('test helper methods', () => { expect(buildMutedRulesFilter(rulesStates)).toEqual(expectedQuery); }); }); + + describe('buildEntityFlyoutPreviewQueryTest', () => { + it('should return the correct query when given field and query', () => { + const field = 'host.name'; + const query = 'exampleHost'; + const expectedQuery = { + bool: { + filter: [ + { + bool: { + should: [{ term: { 'host.name': { value: 'exampleHost' } } }], + minimum_should_match: 1, + }, + }, + ], + }, + }; + + expect(buildEntityFlyoutPreviewQuery(field, query)).toEqual(expectedQuery); + }); + + it('should return the correct query when given field and empty query', () => { + const field = 'host.name'; + const expectedQuery = { + bool: { + filter: [ + { + bool: { + should: [{ term: { 'host.name': { value: '' } } }], + minimum_should_match: 1, + }, + }, + ], + }, + }; + + expect(buildEntityFlyoutPreviewQuery(field)).toEqual(expectedQuery); + }); + }); }); diff --git a/x-pack/packages/kbn-cloud-security-posture-common/utils/helpers.ts b/x-pack/packages/kbn-cloud-security-posture-common/utils/helpers.ts index 7c26c61778137..1c593fcebf545 100644 --- a/x-pack/packages/kbn-cloud-security-posture-common/utils/helpers.ts +++ b/x-pack/packages/kbn-cloud-security-posture-common/utils/helpers.ts @@ -41,3 +41,18 @@ export const buildMutedRulesFilter = ( return mutedRulesFilterQuery; }; + +export const buildEntityFlyoutPreviewQuery = (field: string, queryValue?: string) => { + return { + bool: { + filter: [ + { + bool: { + should: [{ term: { [field]: { value: `${queryValue || ''}` } } }], + minimum_should_match: 1, + }, + }, + ], + }, + }; +}; diff --git a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_preview.ts b/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_preview.ts new file mode 100644 index 0000000000000..af7371da95301 --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_preview.ts @@ -0,0 +1,149 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; +import { lastValueFrom } from 'rxjs'; +import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + CDR_MISCONFIGURATIONS_INDEX_PATTERN, + LATEST_FINDINGS_RETENTION_POLICY, + CspFinding, +} from '@kbn/cloud-security-posture-common'; +import type { CspBenchmarkRulesStates } from '@kbn/cloud-security-posture-common/schema/rules/latest'; +import { buildMutedRulesFilter } from '@kbn/cloud-security-posture-common'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { CoreStart } from '@kbn/core/public'; +import { showErrorToast } from '../..'; +import type { CspClientPluginStartDeps } from '../../type'; +import { useGetCspBenchmarkRulesStatesApi } from './use_get_benchmark_rules_state_api'; + +interface MisconfigurationPreviewBaseEsQuery { + query?: { + bool: { + filter: estypes.QueryDslQueryContainer[]; + }; + }; +} + +interface UseMisconfigurationPreviewOptions extends MisconfigurationPreviewBaseEsQuery { + sort: string[][]; + enabled: boolean; + pageSize: number; +} + +type LatestFindingsRequest = IKibanaSearchRequest; +type LatestFindingsResponse = IKibanaSearchResponse< + estypes.SearchResponse +>; + +interface FindingsAggs { + count: estypes.AggregationsMultiBucketAggregateBase; +} + +const RESULT_EVALUATION = { + PASSED: 'passed', + FAILED: 'failed', + UNKNOWN: 'unknown', +}; + +export const getFindingsCountAggQueryMisconfigurationPreview = () => ({ + count: { + filters: { + other_bucket_key: RESULT_EVALUATION.UNKNOWN, + filters: { + [RESULT_EVALUATION.PASSED]: { match: { 'result.evaluation': RESULT_EVALUATION.PASSED } }, + [RESULT_EVALUATION.FAILED]: { match: { 'result.evaluation': RESULT_EVALUATION.FAILED } }, + }, + }, + }, +}); + +export const getMisconfigurationAggregationCount = ( + buckets: estypes.AggregationsBuckets +) => { + return Object.entries(buckets).reduce( + (evaluation, [key, value]) => { + evaluation[key] = (evaluation[key] || 0) + (value.doc_count || 0); + return evaluation; + }, + { + [RESULT_EVALUATION.PASSED]: 0, + [RESULT_EVALUATION.FAILED]: 0, + [RESULT_EVALUATION.UNKNOWN]: 0, + } + ); +}; + +export const buildMisconfigurationsFindingsQuery = ( + { query }: UseMisconfigurationPreviewOptions, + rulesStates: CspBenchmarkRulesStates +) => { + const mutedRulesFilterQuery = buildMutedRulesFilter(rulesStates); + + return { + index: CDR_MISCONFIGURATIONS_INDEX_PATTERN, + size: 0, + aggs: getFindingsCountAggQueryMisconfigurationPreview(), + ignore_unavailable: false, + query: buildMisconfigurationsFindingsQueryWithFilters(query, mutedRulesFilterQuery), + }; +}; + +const buildMisconfigurationsFindingsQueryWithFilters = ( + query: UseMisconfigurationPreviewOptions['query'], + mutedRulesFilterQuery: estypes.QueryDslQueryContainer[] +) => { + return { + ...query, + bool: { + ...query?.bool, + filter: [ + ...(query?.bool?.filter ?? []), + { + range: { + '@timestamp': { + gte: `now-${LATEST_FINDINGS_RETENTION_POLICY}`, + lte: 'now', + }, + }, + }, + ], + must_not: [...mutedRulesFilterQuery], + }, + }; +}; + +export const useMisconfigurationPreview = (options: UseMisconfigurationPreviewOptions) => { + const { + data, + notifications: { toasts }, + } = useKibana().services; + const { data: rulesStates } = useGetCspBenchmarkRulesStatesApi(); + + return useQuery( + ['csp_misconfiguration_preview', { params: options }, rulesStates], + async () => { + const { + rawResponse: { aggregations }, + } = await lastValueFrom( + data.search.search({ + params: buildMisconfigurationsFindingsQuery(options, rulesStates!), + }) + ); + if (!aggregations) throw new Error('expected aggregations to be defined'); + + return { + count: getMisconfigurationAggregationCount(aggregations.count.buckets), + }; + }, + { + enabled: options.enabled && !!rulesStates, + keepPreviousData: true, + onError: (err: Error) => showErrorToast(toasts, err), + } + ); +}; diff --git a/x-pack/packages/kbn-cloud-security-posture/tsconfig.json b/x-pack/packages/kbn-cloud-security-posture/tsconfig.json index 152c3fe8a8b29..fec5d21b8aa79 100644 --- a/x-pack/packages/kbn-cloud-security-posture/tsconfig.json +++ b/x-pack/packages/kbn-cloud-security-posture/tsconfig.json @@ -36,5 +36,6 @@ "@kbn/kibana-react-plugin", "@kbn/cloud-security-posture-common", "@kbn/i18n", + "@kbn/search-types", ] } diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/index.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/index.tsx new file mode 100644 index 0000000000000..25b953e1b1547 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/index.tsx @@ -0,0 +1,52 @@ +/* + * 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 { EuiAccordion, EuiSpacer, EuiTitle, useEuiTheme } from '@elastic/eui'; + +import React from 'react'; +import { css } from '@emotion/react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useCspSetupStatusApi } from '@kbn/cloud-security-posture/src/hooks/use_csp_setup_status_api'; +import { MisconfigurationsPreview } from './misconfiguration/misconfiguration_preview'; + +export const EntityInsight = ({ hostName }: { hostName: string }) => { + const { euiTheme } = useEuiTheme(); + const getSetupStatus = useCspSetupStatusApi(); + const hasMisconfigurationFindings = getSetupStatus.data?.hasMisconfigurationsFindings; + + return ( + <> + {hasMisconfigurationFindings && ( + +

+ +

+ + } + > + + + +
+ )} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.test.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.test.tsx new file mode 100644 index 0000000000000..1c4c2adb60218 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.test.tsx @@ -0,0 +1,28 @@ +/* + * 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. + */ + +// Add stuff here +import { TestProviders } from '../../../common/mock'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { MisconfigurationsPreview } from './misconfiguration_preview'; + +const mockProps = { + hostName: 'testContextID', +}; + +describe('MisconfigurationsPreview', () => { + it('renders', () => { + const { queryByTestId } = render(, { + wrapper: TestProviders, + }); + expect( + queryByTestId('securitySolutionFlyoutInsightsMisconfigurationsContent') + ).toBeInTheDocument(); + expect(queryByTestId('noFindingsDataTestSubj')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx new file mode 100644 index 0000000000000..3ae2acadcf8e4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { css } from '@emotion/react'; +import type { EuiThemeComputed } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, useEuiTheme, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { DistributionBar } from '@kbn/security-solution-distribution-bar'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { i18n } from '@kbn/i18n'; +import { ExpandablePanel } from '@kbn/security-solution-common'; +import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; + +const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => { + if (passedFindingsStats === 0 && failedFindingsStats === 0) return []; + return [ + { + key: i18n.translate( + 'xpack.securitySolution.flyout.right.insights.misconfigurations.passedFindingsText', + { + defaultMessage: 'Passed findings', + } + ), + count: passedFindingsStats, + color: euiThemeVars.euiColorSuccess, + }, + { + key: i18n.translate( + 'xpack.securitySolution.flyout.right.insights.misconfigurations.failedFindingsText', + { + defaultMessage: 'Failed findings', + } + ), + count: failedFindingsStats, + color: euiThemeVars.euiColorVis9, + }, + ]; +}; + +const MisconfigurationEmptyState = ({ euiTheme }: { euiTheme: EuiThemeComputed<{}> }) => { + return ( + + + + +

{'-'}

+
+
+ + + + + +
+
+ ); +}; + +const MisconfigurationPreviewScore = ({ + passedFindings, + failedFindings, + euiTheme, +}: { + passedFindings: number; + failedFindings: number; + euiTheme: EuiThemeComputed<{}>; +}) => { + return ( + + + + +

{`${Math.round((passedFindings / (passedFindings + failedFindings)) * 100)}%`}

+
+
+ + + + + +
+
+ ); +}; + +export const MisconfigurationsPreview = ({ hostName }: { hostName: string }) => { + const { data } = useMisconfigurationPreview({ + query: buildEntityFlyoutPreviewQuery('host.name', hostName), + sort: [], + enabled: true, + pageSize: 1, + }); + + const passedFindings = data?.count.passed || 0; + const failedFindings = data?.count.failed || 0; + + const { euiTheme } = useEuiTheme(); + const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0; + return ( + + + + ), + }} + data-test-subj={'securitySolutionFlyoutInsightsMisconfigurations'} + > + + {hasMisconfigurationFindings ? ( + + ) : ( + + )} + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/jest.config.js b/x-pack/plugins/security_solution/public/cloud_security_posture/jest.config.js new file mode 100644 index 0000000000000..6ef34a542da9f --- /dev/null +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/jest.config.js @@ -0,0 +1,19 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/cloud_security_posture'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/cloud_security_posture', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/security_solution/public/cloud_security_posture/**/*.{ts,tsx}', + ], + moduleNameMapper: require('../../server/__mocks__/module_name_map'), +}; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx index 27e824d39c913..ea149f3e7ede7 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/content.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { EuiHorizontalRule } from '@elastic/eui'; import { FlyoutBody } from '@kbn/security-solution-common'; +import { EntityInsight } from '../../../cloud_security_posture/components'; import { AssetCriticalityAccordion } from '../../../entity_analytics/components/asset_criticality/asset_criticality_selector'; import { FlyoutRiskSummary } from '../../../entity_analytics/components/risk_summary_flyout/risk_summary'; import type { RiskScoreState } from '../../../entity_analytics/api/hooks/use_risk_score'; @@ -71,6 +72,8 @@ export const HostPanelContent = ({ observedFields={observedFields} queryId={HOST_PANEL_OBSERVED_HOST_QUERY_ID} /> + + ); }; diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index a2a9fda8ecc44..7a97f239364c8 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -218,5 +218,8 @@ "@kbn/config", "@kbn/cbor", "@kbn/zod", + "@kbn/cloud-security-posture", + "@kbn/security-solution-distribution-bar", + "@kbn/cloud-security-posture-common", ] }