From c036774105eb976ded3485e03bbe46e55b3de565 Mon Sep 17 00:00:00 2001 From: Rickyanto Ang Date: Wed, 22 Mar 2023 17:24:04 -0700 Subject: [PATCH 1/3] [Cloud Posture] Full dashboard onboarding for both integrations (#150134) ## Summary Tasks to do 1. Change the /status API response to accommodate 2 different CSP integration ( cspm and kspm ) 2. Fix the issue on CSP Dashboard where after user install kspm integration, you'll see installation prompt but without cspm/kspm tabs ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../common/constants.ts | 2 + .../cloud_security_posture/common/types.ts | 29 +- .../public/common/api/use_setup_status_api.ts | 2 +- .../components/cloud_posture_page.test.tsx | 9 +- .../public/components/cloud_posture_page.tsx | 7 +- .../public/components/no_findings_states.tsx | 13 +- .../compliance_dashboard.test.tsx | 152 ++++- .../compliance_dashboard.tsx | 116 ++-- .../configurations/configurations.test.tsx | 46 +- .../pages/configurations/configurations.tsx | 8 +- .../public/pages/findings/findings.tsx | 9 + .../server/lib/check_index_status.ts | 22 +- .../server/lib/fleet_util.ts | 32 +- .../routes/benchmarks/benchmarks.test.ts | 71 ++- .../server/routes/benchmarks/benchmarks.ts | 4 +- .../server/routes/status/status.test.ts | 563 ++++-------------- .../server/routes/status/status.ts | 150 ++++- .../cloud_security_posture/tsconfig.json | 1 - .../apis/cloud_security_posture/status.ts | 14 +- .../pages/findings.ts | 5 + 20 files changed, 624 insertions(+), 631 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 39a1cbf2fd32..04e31f9e0b64 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -47,6 +47,8 @@ export const CSP_LATEST_VULNERABILITIES_INGEST_TIMESTAMP_PIPELINE = export const RULE_PASSED = `passed`; export const RULE_FAILED = `failed`; +export const POSTURE_TYPE_ALL = 'all'; + // A mapping of in-development features to their status. These features should be hidden from users but can be easily // activated via a simple code change in a single location. export const INTERNAL_FEATURE_FLAGS = { diff --git a/x-pack/plugins/cloud_security_posture/common/types.ts b/x-pack/plugins/cloud_security_posture/common/types.ts index dc229560b502..83fd9a4b276c 100644 --- a/x-pack/plugins/cloud_security_posture/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/common/types.ts @@ -11,6 +11,8 @@ import { SUPPORTED_CLOUDBEAT_INPUTS, SUPPORTED_POLICY_TEMPLATES } from './consta import { CspRuleTemplateMetadata } from './schemas/csp_rule_template_metadata'; export type Evaluation = 'passed' | 'failed' | 'NA'; + +export type PostureTypes = 'cspm' | 'kspm' | 'all'; /** number between 1-100 */ export type Score = number; @@ -59,7 +61,8 @@ export type CspStatusCode = | 'unprivileged' // user lacks privileges for the latest findings index | 'index-timeout' // index timeout was surpassed since installation | 'not-deployed' // no healthy agents were deployed - | 'not-installed'; // number of installed csp integrations is 0; + | 'not-installed' // number of installed csp integrations is 0; + | 'waiting_for_results'; // have healthy agents but no findings at all, assumes data is being indexed for the 1st time export type IndexStatus = | 'not-empty' // Index contains documents @@ -71,27 +74,21 @@ export interface IndexDetails { status: IndexStatus; } -interface BaseCspSetupStatus { - indicesDetails: IndexDetails[]; - latestPackageVersion: string; +interface BaseCspSetupBothPolicy { + status: CspStatusCode; installedPackagePolicies: number; healthyAgents: number; - isPluginInitialized: boolean; - installedPolicyTemplates: CloudSecurityPolicyTemplate[]; } -interface CspSetupNotInstalledStatus extends BaseCspSetupStatus { - status: Extract; -} - -interface CspSetupInstalledStatus extends BaseCspSetupStatus { - status: Exclude; - // if installedPackageVersion == undefined but status != 'not-installed' it means the integration was installed in the past and findings were found - // status can be `indexed` but return with undefined package information in this case - installedPackageVersion: string | undefined; +export interface BaseCspSetupStatus { + indicesDetails: IndexDetails[]; + latestPackageVersion: string; + cspm: BaseCspSetupBothPolicy; + kspm: BaseCspSetupBothPolicy; + isPluginInitialized: boolean; } -export type CspSetupStatus = CspSetupInstalledStatus | CspSetupNotInstalledStatus; +export type CspSetupStatus = BaseCspSetupStatus; export type AgentPolicyStatus = Pick & { agents: number }; diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/use_setup_status_api.ts b/x-pack/plugins/cloud_security_posture/public/common/api/use_setup_status_api.ts index 7c5d4eb8dc31..31edc058dec5 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/api/use_setup_status_api.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/api/use_setup_status_api.ts @@ -7,7 +7,7 @@ import { useQuery, type UseQueryOptions } from '@tanstack/react-query'; import { useKibana } from '../hooks/use_kibana'; -import { CspSetupStatus } from '../../../common/types'; +import { type CspSetupStatus } from '../../../common/types'; import { STATUS_ROUTE_PATH } from '../../../common/constants'; const getCspSetupStatusQueryKey = 'csp_status_key'; diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx index 749aa1ccb038..4d6ec61ea692 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx @@ -144,7 +144,14 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'not-installed' }, + data: { + kspm: { status: 'not-installed' }, + cspm: { status: 'not-installed' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'empty' }, + ], + }, }) ); (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx index 8d4cf6773dfc..a697b1219012 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx @@ -275,7 +275,12 @@ export const CloudPosturePage = ({ return defaultLoadingRenderer(); } - if (getSetupStatus.data.status === 'not-installed') { + /* Checks if its a completely new user which means no integration has been installed and no latest findings default index has been found */ + if ( + getSetupStatus.data?.kspm?.status === 'not-installed' && + getSetupStatus.data?.cspm?.status === 'not-installed' && + getSetupStatus.data?.indicesDetails[0].status === 'empty' + ) { return packageNotInstalledRenderer({ kspmIntegrationLink, cspmIntegrationLink }); } diff --git a/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx b/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx index f8f2c9dc41e9..8109baf738a1 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/no_findings_states.tsx @@ -23,10 +23,14 @@ import { useCISIntegrationPoliciesLink } from '../common/navigation/use_navigate import { NO_FINDINGS_STATUS_TEST_SUBJ } from './test_subjects'; import { CloudPosturePage } from './cloud_posture_page'; import { useCspSetupStatusApi } from '../common/api/use_setup_status_api'; -import type { IndexDetails } from '../../common/types'; +import type { CloudSecurityPolicyTemplate, IndexDetails } from '../../common/types'; const REFETCH_INTERVAL_MS = 20000; +interface PostureTypes { + posturetype: CloudSecurityPolicyTemplate; +} + const NotDeployed = () => { // using an existing hook to get agent id and package policy id const benchmarks = useCspBenchmarkIntegrations({ @@ -176,19 +180,20 @@ const Unprivileged = ({ unprivilegedIndices }: { unprivilegedIndices: string[] } * This component will return the render states based on cloud posture setup status API * since 'not-installed' is being checked globally by CloudPosturePage and 'indexed' is the pass condition, those states won't be handled here * */ -export const NoFindingsStates = () => { +export const NoFindingsStates = (posturetype?: PostureTypes) => { const getSetupStatus = useCspSetupStatusApi({ options: { refetchInterval: REFETCH_INTERVAL_MS }, }); - const status = getSetupStatus.data?.status; + const statusKspm = getSetupStatus.data?.kspm?.status; + const statusCspm = getSetupStatus.data?.cspm?.status; const indicesStatus = getSetupStatus.data?.indicesDetails; + const status = posturetype?.posturetype === 'cspm' ? statusCspm : statusKspm; const unprivilegedIndices = indicesStatus && indicesStatus .filter((idxDetails) => idxDetails.status === 'unprivileged') .map((idxDetails: IndexDetails) => idxDetails.index) .sort((a, b) => a.localeCompare(b)); - const render = () => { if (status === 'not-deployed') return ; // integration installed, but no agents added if (status === 'indexing') return ; // agent added, index timeout hasn't passed since installation diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx index b13deea52746..bf98aff994bc 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import Chance from 'chance'; + import { coreMock } from '@kbn/core/public/mocks'; import { render } from '@testing-library/react'; import { TestProvider } from '../../test/test_provider'; @@ -22,8 +22,6 @@ import { import { mockDashboardData } from './mock'; import { createReactQueryResponse } from '../../test/fixtures/react_query'; import { NO_FINDINGS_STATUS_TEST_SUBJ } from '../../components/test_subjects'; -import { useCISIntegrationPoliciesLink } from '../../common/navigation/use_navigate_to_cis_integration_policies'; -import { useCspIntegrationLink } from '../../common/navigation/use_csp_integration_link'; import { expectIdsInDoc } from '../../test/utils'; jest.mock('../../common/api/use_setup_status_api'); @@ -32,8 +30,6 @@ jest.mock('../../common/hooks/use_subscription_status'); jest.mock('../../common/navigation/use_navigate_to_cis_integration_policies'); jest.mock('../../common/navigation/use_csp_integration_link'); -const chance = new Chance(); - describe('', () => { beforeEach(() => { jest.resetAllMocks(); @@ -89,18 +85,32 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'not-deployed', installedPolicyTemplates: [] }, + data: { + kspm: { status: 'not-deployed', healthyAgents: 0, installedPackagePolicies: 1 }, + cspm: { status: 'not-deployed', healthyAgents: 0, installedPackagePolicies: 1 }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'empty' }, + ], + }, }) ); - (useCISIntegrationPoliciesLink as jest.Mock).mockImplementation(() => chance.url()); - (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 0 } }, + })); + (useCspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 0 } }, + })); renderComplianceDashboardPage(); expectIdsInDoc({ be: [NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED], notToBe: [ - DASHBOARD_CONTAINER, NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, @@ -112,17 +122,32 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'indexing', installedPolicyTemplates: [] }, + data: { + kspm: { status: 'indexing', healthyAgents: 1, installedPackagePolicies: 1 }, + cspm: { status: 'indexing', healthyAgents: 1, installedPackagePolicies: 1 }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'empty' }, + ], + }, }) ); - (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 1 } }, + })); + (useCspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 1 } }, + })); renderComplianceDashboardPage(); expectIdsInDoc({ be: [NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING], notToBe: [ - DASHBOARD_CONTAINER, NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, @@ -134,17 +159,32 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'index-timeout', installedPolicyTemplates: [] }, + data: { + kspm: { status: 'index-timeout', healthyAgents: 1, installedPackagePolicies: 1 }, + cspm: { status: 'index-timeout', healthyAgents: 1, installedPackagePolicies: 1 }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'empty' }, + ], + }, }) ); - (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 0 } }, + })); + (useCspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 0 } }, + })); renderComplianceDashboardPage(); expectIdsInDoc({ be: [NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT], notToBe: [ - DASHBOARD_CONTAINER, NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, @@ -156,17 +196,32 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'unprivileged', installedPolicyTemplates: [] }, + data: { + kspm: { status: 'unprivileged', healthyAgents: 1, installedPackagePolicies: 1 }, + cspm: { status: 'unprivileged', healthyAgents: 1, installedPackagePolicies: 1 }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'empty' }, + ], + }, }) ); - (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 0 } }, + })); + (useCspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 0 } }, + })); renderComplianceDashboardPage(); expectIdsInDoc({ be: [NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED], notToBe: [ - DASHBOARD_CONTAINER, NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, @@ -178,7 +233,14 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'indexed', installedPolicyTemplates: ['cspm', 'kspm'] }, + data: { + kspm: { status: 'indexed' }, + cspm: { status: 'indexed' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' }, + ], + }, }) ); (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ @@ -209,7 +271,14 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'indexed', installedPolicyTemplates: ['cspm', 'kspm'] }, + data: { + kspm: { status: 'indexed' }, + cspm: { status: 'not-installed' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' }, + ], + }, }) ); (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ @@ -241,7 +310,13 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'indexed', installedPolicyTemplates: ['cspm', 'kspm'] }, + data: { + cspm: { status: 'indexed' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' }, + ], + }, }) ); (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ @@ -273,7 +348,13 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'indexed', installedPolicyTemplates: ['cspm'] }, + data: { + cspm: { status: 'indexed', healthyAgents: 0, installedPackagePolicies: 1 }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' }, + ], + }, }) ); (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ @@ -305,7 +386,14 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'indexed', installedPolicyTemplates: ['kspm'] }, + data: { + kspm: { status: 'indexed', healthyAgents: 0, installedPackagePolicies: 1 }, + cspm: { status: 'not-installed' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'empty' }, + ], + }, }) ); (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ @@ -337,7 +425,14 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'indexed', installedPolicyTemplates: ['cspm', 'kspm'] }, + data: { + cspm: { status: 'indexed' }, + kspm: { status: 'indexed' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' }, + ], + }, }) ); (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ @@ -369,7 +464,14 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'indexed', installedPolicyTemplates: ['cspm', 'kspm'] }, + data: { + cspm: { status: 'indexed' }, + kspm: { status: 'indexed' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'not-empty' }, + ], + }, }) ); (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx index be2f5872a2da..346ed041520d 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx @@ -128,7 +128,7 @@ const IntegrationPostureDashboard = ({ } // integration is installed, but there are no findings for this integration - if (noFindings) { + if (noFindings && isIntegrationInstalled) { return ( // height is calculated for the screen height minus the kibana header, page title, and tabs
- + - + ); @@ -177,23 +177,28 @@ const IntegrationPostureDashboard = ({ export const ComplianceDashboard = () => { const [selectedTab, setSelectedTab] = useState(CSPM_POLICY_TEMPLATE); const getSetupStatus = useCspSetupStatusApi(); - const hasFindings = getSetupStatus.data?.status === 'indexed'; + const hasFindingsKspm = + getSetupStatus.data?.kspm?.status === 'indexed' || + getSetupStatus.data?.indicesDetails[0].status === 'not-empty'; + const hasFindingsCspm = + getSetupStatus.data?.cspm?.status === 'indexed' || + getSetupStatus.data?.indicesDetails[0].status === 'not-empty'; const cspmIntegrationLink = useCspIntegrationLink(CSPM_POLICY_TEMPLATE); const kspmIntegrationLink = useCspIntegrationLink(KSPM_POLICY_TEMPLATE); const getCspmDashboardData = useCspmStatsApi({ - enabled: hasFindings, + enabled: hasFindingsCspm, }); const getKspmDashboardData = useKspmStatsApi({ - enabled: hasFindings, + enabled: hasFindingsKspm, }); useEffect(() => { const selectInitialTab = () => { const cspmTotalFindings = getCspmDashboardData.data?.stats.totalFindings; const kspmTotalFindings = getKspmDashboardData.data?.stats.totalFindings; - const installedPolicyTemplates = getSetupStatus.data?.installedPolicyTemplates; - + const installedPolicyTemplatesCspm = getSetupStatus.data?.cspm?.status; + const installedPolicyTemplatesKspm = getSetupStatus.data?.kspm?.status; let preferredDashboard = CSPM_POLICY_TEMPLATE; // cspm has findings @@ -205,21 +210,27 @@ export const ComplianceDashboard = () => { preferredDashboard = KSPM_POLICY_TEMPLATE; } // cspm is installed - else if (installedPolicyTemplates?.includes(CSPM_POLICY_TEMPLATE)) { + else if ( + installedPolicyTemplatesCspm !== 'unprivileged' && + installedPolicyTemplatesCspm !== 'not-installed' + ) { preferredDashboard = CSPM_POLICY_TEMPLATE; } // kspm is installed - else if (installedPolicyTemplates?.includes(KSPM_POLICY_TEMPLATE)) { + else if ( + installedPolicyTemplatesKspm !== 'unprivileged' && + installedPolicyTemplatesKspm !== 'not-installed' + ) { preferredDashboard = KSPM_POLICY_TEMPLATE; } - setSelectedTab(preferredDashboard); }; selectInitialTab(); }, [ getCspmDashboardData.data?.stats.totalFindings, getKspmDashboardData.data?.stats.totalFindings, - getSetupStatus.data?.installedPolicyTemplates, + getSetupStatus.data?.cspm?.status, + getSetupStatus.data?.kspm?.status, ]); const tabs = useMemo( @@ -231,21 +242,28 @@ export const ComplianceDashboard = () => { isSelected: selectedTab === CSPM_POLICY_TEMPLATE, onClick: () => setSelectedTab(CSPM_POLICY_TEMPLATE), content: ( - -
- -
-
+ <> + {hasFindingsCspm ? ( + +
+ +
+
+ ) : ( + + )} + ), }, { @@ -255,21 +273,28 @@ export const ComplianceDashboard = () => { isSelected: selectedTab === KSPM_POLICY_TEMPLATE, onClick: () => setSelectedTab(KSPM_POLICY_TEMPLATE), content: ( - -
- -
-
+ <> + {hasFindingsKspm ? ( + +
+ +
+
+ ) : ( + + )} + ), }, ], @@ -277,14 +302,15 @@ export const ComplianceDashboard = () => { cspmIntegrationLink, getCspmDashboardData, getKspmDashboardData, - getSetupStatus.data?.installedPolicyTemplates, + getSetupStatus.data?.kspm?.status, + getSetupStatus.data?.cspm?.status, kspmIntegrationLink, selectedTab, + hasFindingsKspm, + hasFindingsCspm, ] ); - if (!hasFindings) return ; - return ( ', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'not-deployed' }, + data: { + kspm: { status: 'not-deployed' }, + cspm: { status: 'not-deployed' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'empty' }, + ], + }, }) ); (useCISIntegrationPoliciesLink as jest.Mock).mockImplementation(() => chance.url()); @@ -94,7 +101,14 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'indexing' }, + data: { + kspm: { status: 'indexing' }, + cspm: { status: 'indexing' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'empty' }, + ], + }, }) ); (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); @@ -116,7 +130,14 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'index-timeout' }, + data: { + kspm: { status: 'index-timeout' }, + cspm: { status: 'index-timeout' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'empty' }, + ], + }, }) ); (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); @@ -138,7 +159,14 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'unprivileged' }, + data: { + kspm: { status: 'unprivileged' }, + cspm: { status: 'unprivileged' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'empty' }, + ], + }, }) ); (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); @@ -161,7 +189,15 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => ({ status: 'success', - data: { status: 'indexed' }, + data: { + kspm: { status: 'indexed' }, + cspm: { status: 'indexed' }, + indicesDetails: [ + { index: 'logs-cloud_security_posture.findings_latest-default', status: 'not-empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'empty' }, + { index: 'logs-cloud_security_posture.findings-default*', status: 'empty' }, + ], + }, })); (source.fetch$ as jest.Mock).mockReturnValue(of({ rawResponse: { hits: { hits: [] } } })); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx index 09983e7a6cc6..e82a38e006ce 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx @@ -20,9 +20,11 @@ export const Configurations = () => { const location = useLocation(); const dataViewQuery = useLatestFindingsDataView(); const getSetupStatus = useCspSetupStatusApi(); - - const hasFindings = getSetupStatus.data?.status === 'indexed'; - if (!hasFindings) return ; + const hasFindings = + getSetupStatus.data?.indicesDetails[0].status === 'not-empty' || + getSetupStatus.data?.kspm.status === 'indexed' || + getSetupStatus.data?.cspm.status === 'indexed'; + if (!hasFindings) return ; return ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx index 5840a38d94ef..118cabee9518 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.tsx @@ -18,6 +18,8 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; import { Redirect, Switch, useHistory, useLocation } from 'react-router-dom'; import { Route } from '@kbn/shared-ux-router'; +import { NoFindingsStates } from '../../components/no_findings_states'; +import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api'; import { Configurations } from '../configurations'; import { cloudPosturePages, findingsNavigation } from '../../common/navigation/constants'; import { Vulnerabilities } from '../vulnerabilities'; @@ -25,6 +27,13 @@ import { Vulnerabilities } from '../vulnerabilities'; export const Findings = () => { const history = useHistory(); const location = useLocation(); + const getSetupStatus = useCspSetupStatusApi(); + + const hasFindings = + getSetupStatus.data?.indicesDetails[0].status === 'not-empty' || + getSetupStatus.data?.kspm.status === 'indexed' || + getSetupStatus.data?.cspm.status === 'indexed'; + if (!hasFindings) return ; const navigateToVulnerabilitiesTab = () => { history.push({ pathname: findingsNavigation.vulnerabilities.path }); diff --git a/x-pack/plugins/cloud_security_posture/server/lib/check_index_status.ts b/x-pack/plugins/cloud_security_posture/server/lib/check_index_status.ts index 984c68a76a6b..c8d876a7281d 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/check_index_status.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/check_index_status.ts @@ -11,14 +11,28 @@ import { IndexStatus } from '../../common/types'; export const checkIndexStatus = async ( esClient: ElasticsearchClient, index: string, - logger: Logger + logger: Logger, + postureType: 'cspm' | 'kspm' | 'all' = 'all' ): Promise => { + const query = + postureType === 'all' + ? { + match_all: {}, + } + : { + bool: { + filter: { + term: { + 'rule.benchmark.posture_type': postureType, + }, + }, + }, + }; + try { const queryResult = await esClient.search({ index, - query: { - match_all: {}, - }, + query, size: 1, }); diff --git a/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts b/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts index 1b66a4e40131..2696e44c8c02 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts @@ -18,7 +18,7 @@ import type { PackagePolicy, } from '@kbn/fleet-plugin/common'; import { errors } from '@elastic/elasticsearch'; -import type { CloudSecurityPolicyTemplate } from '../../common/types'; +import { CloudSecurityPolicyTemplate, PostureTypes } from '../../common/types'; import { SUPPORTED_POLICY_TEMPLATES } from '../../common/constants'; import { CSP_FLEET_PACKAGE_KUERY } from '../../common/utils/helpers'; import { @@ -34,13 +34,25 @@ const isFleetMissingAgentHttpError = (error: unknown) => const isPolicyTemplate = (input: any): input is CloudSecurityPolicyTemplate => SUPPORTED_POLICY_TEMPLATES.includes(input); -const getPackageNameQuery = (packageName: string, benchmarkFilter?: string): string => { +const getPackageNameQuery = ( + // ADD 3rd case both cspm and kspm, for findings posture type empty => kspm + postureType: string, + packageName: string, + benchmarkFilter?: string +): string => { const integrationNameQuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageName}`; - const kquery = benchmarkFilter - ? `${integrationNameQuery} AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: *${benchmarkFilter}*` - : integrationNameQuery; - - return kquery; + const integrationPostureType = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.vars.posture.value:${postureType}`; + if (postureType === 'all') { + const kquery = benchmarkFilter + ? `${integrationNameQuery} AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: *${benchmarkFilter}*` + : `${integrationNameQuery}`; + return kquery; + } else { + const kquery = benchmarkFilter + ? `${integrationNameQuery} AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: *${benchmarkFilter}* AND ${integrationPostureType}` + : `${integrationNameQuery} AND ${integrationPostureType}`; + return kquery; + } }; export type AgentStatusByAgentPolicyMap = Record; @@ -86,12 +98,13 @@ export const getCspPackagePolicies = ( soClient: SavedObjectsClientContract, packagePolicyService: PackagePolicyClient, packageName: string, - queryParams: Partial + queryParams: Partial, + postureType: PostureTypes ): Promise> => { const sortField = queryParams.sort_field?.replaceAll(BENCHMARK_PACKAGE_POLICY_PREFIX, ''); return packagePolicyService.list(soClient, { - kuery: getPackageNameQuery(packageName, queryParams.benchmark_name), + kuery: getPackageNameQuery(postureType, packageName, queryParams.benchmark_name), page: queryParams.page, perPage: queryParams.per_page, sortField, @@ -116,7 +129,6 @@ export const getInstalledPolicyTemplates = async ( return policy.inputs.find((input) => input.enabled)?.policy_template; }) .filter(isPolicyTemplate); - // removing duplicates return [...new Set(enabledPolicyTemplates)]; } catch (e) { diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts index d8b432bb0391..210e60919fbc 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts @@ -14,6 +14,7 @@ import { getCspPackagePolicies, getCspAgentPolicies, } from '../../lib/fleet_util'; +import { POSTURE_TYPE_ALL } from '../../../common/constants'; import { defineGetBenchmarksRoute, getRulesCountForPolicy } from './benchmarks'; import { SavedObjectsClientContract, SavedObjectsFindResponse } from '@kbn/core/server'; @@ -157,11 +158,17 @@ describe('benchmarks API', () => { it('should format request by package name', async () => { const mockPackagePolicyService = createPackagePolicyServiceMock(); - await getCspPackagePolicies(mockSoClient, mockPackagePolicyService, 'myPackage', { - page: 1, - per_page: 100, - sort_order: 'desc', - }); + await getCspPackagePolicies( + mockSoClient, + mockPackagePolicyService, + 'myPackage', + { + page: 1, + per_page: 100, + sort_order: 'desc', + }, + POSTURE_TYPE_ALL + ); expect(mockPackagePolicyService.list.mock.calls[0][1]).toMatchObject( expect.objectContaining({ @@ -175,12 +182,18 @@ describe('benchmarks API', () => { it('should build sort request by `sort_field` and default `sort_order`', async () => { const mockAgentPolicyService = createPackagePolicyServiceMock(); - await getCspPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { - page: 1, - per_page: 100, - sort_field: 'package_policy.name', - sort_order: 'desc', - }); + await getCspPackagePolicies( + mockSoClient, + mockAgentPolicyService, + 'myPackage', + { + page: 1, + per_page: 100, + sort_field: 'package_policy.name', + sort_order: 'desc', + }, + POSTURE_TYPE_ALL + ); expect(mockAgentPolicyService.list.mock.calls[0][1]).toMatchObject( expect.objectContaining({ @@ -196,12 +209,18 @@ describe('benchmarks API', () => { it('should build sort request by `sort_field` and asc `sort_order`', async () => { const mockAgentPolicyService = createPackagePolicyServiceMock(); - await getCspPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { - page: 1, - per_page: 100, - sort_field: 'package_policy.name', - sort_order: 'asc', - }); + await getCspPackagePolicies( + mockSoClient, + mockAgentPolicyService, + 'myPackage', + { + page: 1, + per_page: 100, + sort_field: 'package_policy.name', + sort_order: 'asc', + }, + POSTURE_TYPE_ALL + ); expect(mockAgentPolicyService.list.mock.calls[0][1]).toMatchObject( expect.objectContaining({ @@ -218,12 +237,18 @@ describe('benchmarks API', () => { it('should format request by benchmark_name', async () => { const mockAgentPolicyService = createPackagePolicyServiceMock(); - await getCspPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { - page: 1, - per_page: 100, - sort_order: 'desc', - benchmark_name: 'my_cis_benchmark', - }); + await getCspPackagePolicies( + mockSoClient, + mockAgentPolicyService, + 'myPackage', + { + page: 1, + per_page: 100, + sort_order: 'desc', + benchmark_name: 'my_cis_benchmark', + }, + POSTURE_TYPE_ALL + ); expect(mockAgentPolicyService.list.mock.calls[0][1]).toMatchObject( expect.objectContaining({ diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts index cd019c189c76..6012583104f3 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts @@ -12,6 +12,7 @@ import { CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { BENCHMARKS_ROUTE_PATH, CLOUD_SECURITY_POSTURE_PACKAGE_NAME, + POSTURE_TYPE_ALL, } from '../../../common/constants'; import { benchmarksQueryParamsSchema } from '../../../common/schemas/benchmark'; import type { Benchmark } from '../../../common/types'; @@ -104,7 +105,8 @@ export const defineGetBenchmarksRoute = (router: CspRouter): void => cspContext.soClient, cspContext.packagePolicyService, CLOUD_SECURITY_POSTURE_PACKAGE_NAME, - request.query + request.query, + POSTURE_TYPE_ALL ); const agentPolicies = await getCspAgentPolicies( diff --git a/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts index 98c6536c277d..7f1345ced245 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/status/status.test.ts @@ -5,488 +5,143 @@ * 2.0. */ -import { defineGetCspStatusRoute, INDEX_TIMEOUT_IN_MINUTES } from './status'; -import { httpServerMock, httpServiceMock } from '@kbn/core/server/mocks'; -import type { ESSearchResponse } from '@kbn/es-types'; -import { - AgentClient, - AgentPolicyServiceInterface, - AgentService, - PackageClient, - PackagePolicyClient, - PackageService, -} from '@kbn/fleet-plugin/server'; -import { - AgentPolicy, - GetAgentStatusResponse, - Installation, - RegistryPackage, -} from '@kbn/fleet-plugin/common'; -import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; -import { createCspRequestHandlerContextMock } from '../../mocks'; -import { errors } from '@elastic/elasticsearch'; - -const mockCspPackageInfo: Installation = { - verification_status: 'verified', - installed_kibana: [], - installed_kibana_space_id: 'default', - installed_es: [], - package_assets: [], - es_index_patterns: { findings: 'logs-cloud_security_posture.findings-*' }, - name: 'cloud_security_posture', - version: '0.0.14', - install_version: '0.0.14', - install_status: 'installed', - install_started_at: '2022-06-16T15:24:58.281Z', - install_source: 'registry', -}; - -const mockLatestCspPackageInfo: RegistryPackage = { - format_version: 'mock', - name: 'cloud_security_posture', - title: 'CIS Kubernetes Benchmark', - version: '0.0.14', - release: 'experimental', - description: 'Check Kubernetes cluster compliance with the Kubernetes CIS benchmark.', - type: 'integration', - download: '/epr/cloud_security_posture/cloud_security_posture-0.0.14.zip', - path: '/package/cloud_security_posture/0.0.14', - policy_templates: [], - owner: { github: 'elastic/cloud-security-posture' }, - categories: ['containers', 'kubernetes'], -}; - -describe('CspSetupStatus route', () => { - const router = httpServiceMock.createRouter(); - let mockContext: ReturnType; - let mockPackagePolicyService: jest.Mocked; - let mockAgentPolicyService: jest.Mocked; - let mockAgentService: jest.Mocked; - let mockAgentClient: jest.Mocked; - let mockPackageService: PackageService; - let mockPackageClient: jest.Mocked; - - beforeEach(() => { - jest.clearAllMocks(); - - mockContext = createCspRequestHandlerContextMock(); - mockPackagePolicyService = mockContext.csp.packagePolicyService; - mockAgentPolicyService = mockContext.csp.agentPolicyService; - mockAgentService = mockContext.csp.agentService; - mockPackageService = mockContext.csp.packageService; - - mockAgentClient = mockAgentService.asInternalUser as jest.Mocked; - mockPackageClient = mockPackageService.asInternalUser as jest.Mocked; - }); - - it('validate the API route path', async () => { - defineGetCspStatusRoute(router); - const [config, _] = router.get.mock.calls[0]; - - expect(config.path).toEqual('/internal/cloud_security_posture/status'); - }); - - const indices = [ - { - index: 'logs-cloud_security_posture.findings-default*', - expected_status: 'not-installed', - }, - { - index: 'logs-cloud_security_posture.findings_latest-default', - expected_status: 'unprivileged', - }, - { - index: 'logs-cloud_security_posture.scores-default', - expected_status: 'unprivileged', - }, - ]; - - indices.forEach((idxTestCase) => { - it( - 'Verify the API result when there are no permissions to index: ' + idxTestCase.index, - async () => { - mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseImplementation( - (req) => { - if (req?.index === idxTestCase.index) { - throw new errors.ResponseError({ - body: { - error: { - type: 'security_exception', - }, - }, - statusCode: 503, - headers: {}, - warnings: [], - meta: {} as any, - }); - } - - return { - hits: { - hits: [{}], - }, - } as any; - } - ); - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); - - mockPackagePolicyService.list.mockResolvedValueOnce({ - items: [], - total: 0, - page: 1, - perPage: 100, - }); - - // Act - defineGetCspStatusRoute(router); - const [_, handler] = router.get.mock.calls[0]; - - const mockResponse = httpServerMock.createResponseFactory(); - const mockRequest = httpServerMock.createKibanaRequest(); - await handler(mockContext, mockRequest, mockResponse); - - // Assert - const [call] = mockResponse.ok.mock.calls; - const body = call[0]?.body; - expect(mockResponse.ok).toHaveBeenCalledTimes(1); - - await expect(body).toMatchObject({ - status: idxTestCase.expected_status, - }); - } +import { calculateCspStatusCode } from './status'; +import { CSPM_POLICY_TEMPLATE } from '../../../common/constants'; + +describe('calculateCspStatusCode test', () => { + it('Verify status when there are no permission', async () => { + const statusCode = calculateCspStatusCode( + CSPM_POLICY_TEMPLATE, + { + findingsLatest: 'unprivileged', + findings: 'unprivileged', + score: 'unprivileged', + }, + 1, + 1, + 1, + ['cspm'] ); + + expect(statusCode).toMatch('unprivileged'); }); - it('Verify the API result when there are findings and no installed policies', async () => { - mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ - hits: { - hits: [{ Findings: 'foo' }], + it('Verify status when there are no findings, no healthy agents and no installed policy templates', async () => { + const statusCode = calculateCspStatusCode( + CSPM_POLICY_TEMPLATE, + { + findingsLatest: 'empty', + findings: 'empty', + score: 'empty', }, - } as unknown as ESSearchResponse); - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); - - mockPackagePolicyService.list.mockResolvedValueOnce({ - items: [], - total: 0, - page: 1, - perPage: 100, - }); - - // Act - defineGetCspStatusRoute(router); - const [_, handler] = router.get.mock.calls[0]; - - const mockResponse = httpServerMock.createResponseFactory(); - const mockRequest = httpServerMock.createKibanaRequest(); - await handler(mockContext, mockRequest, mockResponse); - - // Assert - const [call] = mockResponse.ok.mock.calls; - const body = call[0]?.body; - expect(mockResponse.ok).toHaveBeenCalledTimes(1); + 0, + 0, + 0, + [] + ); - await expect(body).toMatchObject({ - status: 'indexed', - latestPackageVersion: '0.0.14', - installedPackagePolicies: 0, - healthyAgents: 0, - installedPackageVersion: undefined, - isPluginInitialized: false, - }); + expect(statusCode).toMatch('not-installed'); }); - it('Verify the API result when there are findings, installed policies, no running agents', async () => { - mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ - hits: { - hits: [{ Findings: 'foo' }], + it('Verify status when there are findings and installed policies but no healthy agents', async () => { + const statusCode = calculateCspStatusCode( + CSPM_POLICY_TEMPLATE, + { + findingsLatest: 'empty', + findings: 'not-empty', + score: 'not-empty', }, - } as unknown as ESSearchResponse); - - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); - mockPackageClient.getInstallation.mockResolvedValueOnce(mockCspPackageInfo); - - mockPackagePolicyService.list.mockResolvedValueOnce({ - items: [], - total: 3, - page: 1, - perPage: 100, - }); - - // Act - defineGetCspStatusRoute(router); - const [_, handler] = router.get.mock.calls[0]; - - const mockResponse = httpServerMock.createResponseFactory(); - const mockRequest = httpServerMock.createKibanaRequest(); - await handler(mockContext, mockRequest, mockResponse); - - // Assert - const [call] = mockResponse.ok.mock.calls; - const body = call[0]?.body; - - expect(mockResponse.ok).toHaveBeenCalledTimes(1); + 1, + 0, + 10, + ['cspm'] + ); - await expect(body).toMatchObject({ - status: 'indexed', - latestPackageVersion: '0.0.14', - installedPackagePolicies: 3, - healthyAgents: 0, - installedPackageVersion: '0.0.14', - isPluginInitialized: false, - }); + expect(statusCode).toMatch('not-deployed'); }); - it('Verify the API result when there are findings, installed policies, running agents', async () => { - mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ - hits: { - hits: [{ Findings: 'foo' }], + it('Verify status when there are findings ,installed policies and healthy agents', async () => { + const statusCode = calculateCspStatusCode( + CSPM_POLICY_TEMPLATE, + { + findingsLatest: 'not-empty', + findings: 'not-empty', + score: 'not-empty', }, - } as unknown as ESSearchResponse); - - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); - mockPackageClient.getInstallation.mockResolvedValueOnce(mockCspPackageInfo); - - mockPackagePolicyService.list.mockResolvedValueOnce({ - items: [], - total: 3, - page: 1, - perPage: 100, - }); - - mockAgentPolicyService.getByIds.mockResolvedValue([ - { package_policies: createPackagePolicyMock() }, - ] as unknown as AgentPolicy[]); - - mockAgentClient.getAgentStatusForAgentPolicy.mockResolvedValue({ - online: 1, - updating: 0, - } as unknown as GetAgentStatusResponse['results']); - - // Act - defineGetCspStatusRoute(router); - const [_, handler] = router.get.mock.calls[0]; - - const mockResponse = httpServerMock.createResponseFactory(); - const mockRequest = httpServerMock.createKibanaRequest(); - await handler(mockContext, mockRequest, mockResponse); - - // Assert - const [call] = mockResponse.ok.mock.calls; - const body = call[0]!.body; - - expect(mockResponse.ok).toHaveBeenCalledTimes(1); + 1, + 1, + 10, + ['cspm'] + ); - await expect(body).toMatchObject({ - status: 'indexed', - latestPackageVersion: '0.0.14', - installedPackagePolicies: 3, - healthyAgents: 1, - installedPackageVersion: '0.0.14', - isPluginInitialized: false, - }); + expect(statusCode).toMatch('indexed'); }); - it('Verify the API result when there are no findings and no installed policies', async () => { - mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ - hits: { - hits: [], + it('Verify status when there are no findings ,installed policies and no healthy agents', async () => { + const statusCode = calculateCspStatusCode( + CSPM_POLICY_TEMPLATE, + { + findingsLatest: 'empty', + findings: 'empty', + score: 'empty', }, - } as unknown as ESSearchResponse); - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); - - mockPackagePolicyService.list.mockResolvedValueOnce({ - items: [], - total: 0, - page: 1, - perPage: 100, - }); - defineGetCspStatusRoute(router); - const [_, handler] = router.get.mock.calls[0]; - - const mockResponse = httpServerMock.createResponseFactory(); - const mockRequest = httpServerMock.createKibanaRequest(); - - // Act - await handler(mockContext, mockRequest, mockResponse); - - // Assert - const [call] = mockResponse.ok.mock.calls; - const body = call[0]!.body; - - expect(mockResponse.ok).toHaveBeenCalledTimes(1); + 1, + 0, + 10, + ['cspm'] + ); - await expect(body).toMatchObject({ - status: 'not-installed', - latestPackageVersion: '0.0.14', - installedPackagePolicies: 0, - healthyAgents: 0, - isPluginInitialized: false, - }); + expect(statusCode).toMatch('not-deployed'); }); - it('Verify the API result when there are no findings, installed agent but no deployed agent', async () => { - mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ - hits: { - hits: [], + it('Verify status when there are installed policies, healthy agents and no findings', async () => { + const statusCode = calculateCspStatusCode( + CSPM_POLICY_TEMPLATE, + { + findingsLatest: 'empty', + findings: 'empty', + score: 'empty', }, - } as unknown as ESSearchResponse); - - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); - mockPackageClient.getInstallation.mockResolvedValueOnce(mockCspPackageInfo); - - mockPackagePolicyService.list.mockResolvedValueOnce({ - items: [], - total: 1, - page: 1, - perPage: 100, - }); - - mockAgentPolicyService.getByIds.mockResolvedValue([ - { package_policies: createPackagePolicyMock() }, - ] as unknown as AgentPolicy[]); - - mockAgentClient.getAgentStatusForAgentPolicy.mockResolvedValue({ - online: 0, - updating: 0, - } as unknown as GetAgentStatusResponse['results']); - - // Act - defineGetCspStatusRoute(router); - - const [_, handler] = router.get.mock.calls[0]; - - const mockResponse = httpServerMock.createResponseFactory(); - const mockRequest = httpServerMock.createKibanaRequest(); - await handler(mockContext, mockRequest, mockResponse); - - // Assert - const [call] = mockResponse.ok.mock.calls; - const body = call[0]!.body; - - expect(mockResponse.ok).toHaveBeenCalledTimes(1); + 1, + 1, + 9, + ['cspm'] + ); - await expect(body).toMatchObject({ - status: 'not-deployed', - latestPackageVersion: '0.0.14', - installedPackagePolicies: 1, - healthyAgents: 0, - installedPackageVersion: '0.0.14', - isPluginInitialized: false, - }); + expect(statusCode).toMatch('waiting_for_results'); }); - it('Verify the API result when there are no findings, installed agent, deployed agent, before index timeout', async () => { - mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ - hits: { - hits: [], + it('Verify status when there are installed policies, healthy agents and no findings and been more than 10 minutes', async () => { + const statusCode = calculateCspStatusCode( + CSPM_POLICY_TEMPLATE, + { + findingsLatest: 'empty', + findings: 'empty', + score: 'empty', }, - } as unknown as ESSearchResponse); - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); - - const currentTime = new Date(); - mockCspPackageInfo.install_started_at = new Date( - currentTime.setMinutes(currentTime.getMinutes() - INDEX_TIMEOUT_IN_MINUTES + 1) - ).toUTCString(); - - mockPackageClient.getInstallation.mockResolvedValueOnce(mockCspPackageInfo); - - mockPackagePolicyService.list.mockResolvedValueOnce({ - items: [], - total: 1, - page: 1, - perPage: 100, - }); - - mockAgentPolicyService.getByIds.mockResolvedValue([ - { package_policies: createPackagePolicyMock() }, - ] as unknown as AgentPolicy[]); - - mockAgentClient.getAgentStatusForAgentPolicy.mockResolvedValue({ - online: 1, - updating: 0, - } as unknown as GetAgentStatusResponse['results']); - - // Act - defineGetCspStatusRoute(router); - - const [_, handler] = router.get.mock.calls[0]; - - const mockResponse = httpServerMock.createResponseFactory(); - const mockRequest = httpServerMock.createKibanaRequest(); - const [context, req, res] = [mockContext, mockRequest, mockResponse]; - - await handler(context, req, res); - - // Assert - const [call] = mockResponse.ok.mock.calls; - const body = call[0]!.body; - - expect(mockResponse.ok).toHaveBeenCalledTimes(1); + 1, + 1, + 11, + ['cspm'] + ); - await expect(body).toMatchObject({ - status: 'indexing', - latestPackageVersion: '0.0.14', - installedPackagePolicies: 1, - healthyAgents: 1, - installedPackageVersion: '0.0.14', - isPluginInitialized: false, - }); + expect(statusCode).toMatch('index-timeout'); }); - it('Verify the API result when there are no findings, installed agent, deployed agent, after index timeout', async () => { - mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ - hits: { - hits: [], + it('Verify status when there are installed policies, healthy agents past findings but no recent findings', async () => { + const statusCode = calculateCspStatusCode( + CSPM_POLICY_TEMPLATE, + { + findingsLatest: 'empty', + findings: 'not-empty', + score: 'not-empty', }, - } as unknown as ESSearchResponse); - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); - - const currentTime = new Date(); - mockCspPackageInfo.install_started_at = new Date( - currentTime.setMinutes(currentTime.getMinutes() - INDEX_TIMEOUT_IN_MINUTES - 1) - ).toUTCString(); - - mockPackageClient.getInstallation.mockResolvedValueOnce(mockCspPackageInfo); - - mockPackagePolicyService.list.mockResolvedValueOnce({ - items: [], - total: 1, - page: 1, - perPage: 100, - }); - - mockAgentPolicyService.getByIds.mockResolvedValue([ - { package_policies: createPackagePolicyMock() }, - ] as unknown as AgentPolicy[]); - - mockAgentClient.getAgentStatusForAgentPolicy.mockResolvedValue({ - online: 1, - updating: 0, - } as unknown as GetAgentStatusResponse['results']); - - // Act - defineGetCspStatusRoute(router); - - const [_, handler] = router.get.mock.calls[0]; - - const mockResponse = httpServerMock.createResponseFactory(); - const mockRequest = httpServerMock.createKibanaRequest(); - - await handler(mockContext, mockRequest, mockResponse); - - // Assert - const [call] = mockResponse.ok.mock.calls; - const body = call[0]!.body; - - expect(mockResponse.ok).toHaveBeenCalledTimes(1); + 1, + 1, + 0, + ['cspm'] + ); - await expect(body).toMatchObject({ - status: 'index-timeout', - latestPackageVersion: '0.0.14', - installedPackagePolicies: 1, - healthyAgents: 1, - installedPackageVersion: '0.0.14', - isPluginInitialized: false, - }); + expect(statusCode).toMatch('indexing'); }); }); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts b/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts index eead1dc267e6..23578194422e 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts @@ -17,9 +17,16 @@ import { LATEST_FINDINGS_INDEX_DEFAULT_NS, FINDINGS_INDEX_PATTERN, BENCHMARK_SCORE_INDEX_DEFAULT_NS, + KSPM_POLICY_TEMPLATE, + CSPM_POLICY_TEMPLATE, } from '../../../common/constants'; import type { CspApiRequestHandlerContext, CspRouter } from '../../types'; -import type { CspSetupStatus, CspStatusCode, IndexStatus } from '../../../common/types'; +import type { + CspSetupStatus, + CspStatusCode, + IndexStatus, + PostureTypes, +} from '../../../common/types'; import { getAgentStatusesByAgentPolicies, getCspAgentPolicies, @@ -60,7 +67,8 @@ const getHealthyAgents = async ( ); }; -const calculateCspStatusCode = ( +export const calculateCspStatusCode = ( + postureType: PostureTypes, indicesStatus: { findingsLatest: IndexStatus; findings: IndexStatus; @@ -68,23 +76,37 @@ const calculateCspStatusCode = ( }, installedCspPackagePolicies: number, healthyAgents: number, - timeSinceInstallationInMinutes: number + timeSinceInstallationInMinutes: number, + installedPolicyTemplates: string[] ): CspStatusCode => { // We check privileges only for the relevant indices for our pages to appear + const postureTypeCheck = + postureType === CSPM_POLICY_TEMPLATE ? CSPM_POLICY_TEMPLATE : KSPM_POLICY_TEMPLATE; if (indicesStatus.findingsLatest === 'unprivileged' || indicesStatus.score === 'unprivileged') return 'unprivileged'; - if (indicesStatus.findingsLatest === 'not-empty') return 'indexed'; - if (installedCspPackagePolicies === 0) return 'not-installed'; + if (!installedPolicyTemplates.includes(postureTypeCheck)) return 'not-installed'; if (healthyAgents === 0) return 'not-deployed'; - if (timeSinceInstallationInMinutes <= INDEX_TIMEOUT_IN_MINUTES) return 'indexing'; - if (timeSinceInstallationInMinutes > INDEX_TIMEOUT_IN_MINUTES) return 'index-timeout'; + if ( + indicesStatus.findingsLatest === 'empty' && + indicesStatus.findings === 'empty' && + timeSinceInstallationInMinutes < INDEX_TIMEOUT_IN_MINUTES + ) + return 'waiting_for_results'; + if ( + indicesStatus.findingsLatest === 'empty' && + indicesStatus.findings === 'empty' && + timeSinceInstallationInMinutes > INDEX_TIMEOUT_IN_MINUTES + ) + return 'index-timeout'; + if (indicesStatus.findingsLatest === 'empty') return 'indexing'; + if (indicesStatus.findings === 'not-empty') return 'indexed'; throw new Error('Could not determine csp status'); }; const assertResponse = (resp: CspSetupStatus, logger: CspApiRequestHandlerContext['logger']) => { if ( - resp.status === 'unprivileged' && + (resp.cspm.status || resp.kspm.status) === 'unprivileged' && !resp.indicesDetails.some((idxDetails) => idxDetails.status === 'unprivileged') ) { logger.warn('Returned status in `unprivileged` but response is missing the unprivileged index'); @@ -105,31 +127,70 @@ const getCspStatus = async ({ findingsLatestIndexStatus, findingsIndexStatus, scoreIndexStatus, + findingsLatestIndexStatusCspm, + findingsIndexStatusCspm, + scoreIndexStatusCspm, + findingsLatestIndexStatusKspm, + findingsIndexStatusKspm, + scoreIndexStatusKspm, installation, latestCspPackage, - installedPackagePolicies, + installedPackagePoliciesKspm, + installedPackagePoliciesCspm, installedPolicyTemplates, ] = await Promise.all([ checkIndexStatus(esClient.asCurrentUser, LATEST_FINDINGS_INDEX_DEFAULT_NS, logger), checkIndexStatus(esClient.asCurrentUser, FINDINGS_INDEX_PATTERN, logger), checkIndexStatus(esClient.asCurrentUser, BENCHMARK_SCORE_INDEX_DEFAULT_NS, logger), + + checkIndexStatus(esClient.asCurrentUser, LATEST_FINDINGS_INDEX_DEFAULT_NS, logger, 'cspm'), + checkIndexStatus(esClient.asCurrentUser, FINDINGS_INDEX_PATTERN, logger, 'cspm'), + checkIndexStatus(esClient.asCurrentUser, BENCHMARK_SCORE_INDEX_DEFAULT_NS, logger, 'cspm'), + + checkIndexStatus(esClient.asCurrentUser, LATEST_FINDINGS_INDEX_DEFAULT_NS, logger, 'kspm'), + checkIndexStatus(esClient.asCurrentUser, FINDINGS_INDEX_PATTERN, logger, 'kspm'), + checkIndexStatus(esClient.asCurrentUser, BENCHMARK_SCORE_INDEX_DEFAULT_NS, logger, 'kspm'), + packageService.asInternalUser.getInstallation(CLOUD_SECURITY_POSTURE_PACKAGE_NAME), packageService.asInternalUser.fetchFindLatestPackage(CLOUD_SECURITY_POSTURE_PACKAGE_NAME), - getCspPackagePolicies(soClient, packagePolicyService, CLOUD_SECURITY_POSTURE_PACKAGE_NAME, { - per_page: 10000, - }), + getCspPackagePolicies( + soClient, + packagePolicyService, + CLOUD_SECURITY_POSTURE_PACKAGE_NAME, + { + per_page: 10000, + }, + KSPM_POLICY_TEMPLATE + ), + getCspPackagePolicies( + soClient, + packagePolicyService, + CLOUD_SECURITY_POSTURE_PACKAGE_NAME, + { + per_page: 10000, + }, + CSPM_POLICY_TEMPLATE + ), getInstalledPolicyTemplates(packagePolicyService, soClient), ]); - const healthyAgents = await getHealthyAgents( + const healthyAgentsKspm = await getHealthyAgents( soClient, - installedPackagePolicies.items, + installedPackagePoliciesKspm.items, agentPolicyService, agentService, logger ); - const installedPackagePoliciesTotal = installedPackagePolicies.total; + const healthyAgentsCspm = await getHealthyAgents( + soClient, + installedPackagePoliciesCspm.items, + agentPolicyService, + agentService, + logger + ); + const installedPackagePoliciesTotalKspm = installedPackagePoliciesKspm.total; + const installedPackagePoliciesTotalCspm = installedPackagePoliciesCspm.total; const latestCspPackageVersion = latestCspPackage.version; const MIN_DATE = 0; @@ -148,35 +209,62 @@ const getCspStatus = async ({ }, ]; - const status = calculateCspStatusCode( + const statusCspm = calculateCspStatusCode( + CSPM_POLICY_TEMPLATE, + { + findingsLatest: findingsLatestIndexStatusCspm, + findings: findingsIndexStatusCspm, + score: scoreIndexStatusCspm, + }, + installedPackagePoliciesTotalCspm, + healthyAgentsCspm, + calculateDiffFromNowInMinutes(installation?.install_started_at || MIN_DATE), + installedPolicyTemplates + ); + + const statusKspm = calculateCspStatusCode( + KSPM_POLICY_TEMPLATE, { - findingsLatest: findingsLatestIndexStatus, - findings: findingsIndexStatus, - score: scoreIndexStatus, + findingsLatest: findingsLatestIndexStatusKspm, + findings: findingsIndexStatusKspm, + score: scoreIndexStatusKspm, }, - installedPackagePoliciesTotal, - healthyAgents, - calculateDiffFromNowInMinutes(installation?.install_started_at || MIN_DATE) + installedPackagePoliciesTotalKspm, + healthyAgentsKspm, + calculateDiffFromNowInMinutes(installation?.install_started_at || MIN_DATE), + installedPolicyTemplates ); - if (status === 'not-installed') + if ((statusCspm && statusKspm) === 'not-installed') return { - status, + cspm: { + status: statusCspm, + healthyAgents: healthyAgentsCspm, + installedPackagePolicies: installedPackagePoliciesTotalCspm, + }, + kspm: { + status: statusKspm, + healthyAgents: healthyAgentsKspm, + installedPackagePolicies: installedPackagePoliciesTotalKspm, + }, indicesDetails, latestPackageVersion: latestCspPackageVersion, - installedPolicyTemplates, - healthyAgents, - installedPackagePolicies: installedPackagePoliciesTotal, isPluginInitialized: isPluginInitialized(), }; const response = { - status, + cspm: { + status: statusCspm, + healthyAgents: healthyAgentsCspm, + installedPackagePolicies: installedPackagePoliciesTotalCspm, + }, + kspm: { + status: statusKspm, + healthyAgents: healthyAgentsKspm, + installedPackagePolicies: installedPackagePoliciesTotalKspm, + }, indicesDetails, latestPackageVersion: latestCspPackageVersion, - healthyAgents, - installedPolicyTemplates, - installedPackagePolicies: installedPackagePoliciesTotal, installedPackageVersion: installation?.install_version, isPluginInitialized: isPluginInitialized(), }; diff --git a/x-pack/plugins/cloud_security_posture/tsconfig.json b/x-pack/plugins/cloud_security_posture/tsconfig.json index 60c540b422bc..a0fe377baf69 100755 --- a/x-pack/plugins/cloud_security_posture/tsconfig.json +++ b/x-pack/plugins/cloud_security_posture/tsconfig.json @@ -42,7 +42,6 @@ "@kbn/utility-types-jest", "@kbn/securitysolution-es-utils", "@kbn/core-elasticsearch-client-server-mocks", - "@kbn/es-types", "@kbn/core-elasticsearch-server", "@kbn/ecs", "@kbn/core-saved-objects-api-server", diff --git a/x-pack/test/api_integration/apis/cloud_security_posture/status.ts b/x-pack/test/api_integration/apis/cloud_security_posture/status.ts index 6d10aa2f60f4..89ba33449ec1 100644 --- a/x-pack/test/api_integration/apis/cloud_security_posture/status.ts +++ b/x-pack/test/api_integration/apis/cloud_security_posture/status.ts @@ -51,9 +51,10 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxxx') .expect(200); - expect(res.status).to.be('not-deployed'); - expect(res.installedPolicyTemplates).length(1).contain('kspm'); - expect(res.healthyAgents).to.be(0); + expect(res.kspm.status).to.be('not-deployed'); + expect(res.cspm.status).to.be('not-installed'); + expect(res.kspm.healthyAgents).to.be(0); + expect(res.kspm.installedPackagePolicies).to.be(1); }); it(`Should return not-deployed when installed cspm`, async () => { @@ -71,9 +72,10 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxxx') .expect(200); - expect(res.status).to.be('not-deployed'); - expect(res.installedPolicyTemplates).length(1).contain('cspm'); - expect(res.healthyAgents).to.be(0); + expect(res.cspm.status).to.be('not-deployed'); + expect(res.kspm.status).to.be('not-installed'); + expect(res.cspm.healthyAgents).to.be(0); + expect(res.cspm.installedPackagePolicies).to.be(1); }); }); } diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings.ts b/x-pack/test/cloud_security_posture_functional/pages/findings.ts index 6a1222ef1e09..010bb788b8c7 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings.ts @@ -29,6 +29,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { section: 'Upper case section', benchmark: { id: 'cis_k8s', + posture_type: 'kspm', name: 'CIS Kubernetes V1.23', version: 'v1.0.0', }, @@ -44,6 +45,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { section: 'Another upper case section', benchmark: { id: 'cis_k8s', + posture_type: 'kspm', name: 'CIS Kubernetes V1.23', version: 'v1.0.0', }, @@ -59,6 +61,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { section: 'lower case section', benchmark: { id: 'cis_k8s', + posture_type: 'kspm', name: 'CIS Kubernetes V1.23', version: 'v1.0.0', }, @@ -74,6 +77,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { section: 'another lower case section', benchmark: { id: 'cis_k8s', + posture_type: 'kspm', name: 'CIS Kubernetes V1.23', version: 'v1.0.0', }, @@ -105,6 +109,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { resourceFindingsTable = findings.resourceFindingsTable; distributionBar = findings.distributionBar; + await findings.index.remove(); await findings.index.add(data); await findings.navigateToLatestFindingsPage(); await retry.waitFor( From a87c7584696ad83791ce53962de1cf3b99f7dc62 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Wed, 22 Mar 2023 18:58:20 -0600 Subject: [PATCH 2/3] [Global settings] Restrict access to users with admin privileges (#153006) ## Summary This PR restricts access to global settings when required capabilities are not in place. The capabilities are: `globalSettings.show` and `globalSettings.save`. ## Testing ### Global All 1. Create a user with the "kibana_admin" role 2. Log in as that user 3. Go to Advanced Settings Should be able to see and save global settings. ### Global Readonly 1. Create a role "kibana_readonly" with Kibana global readonly privileges 2. Create a user with the "kibana_readonly" role 3. Log in as that user 4. Go to Advanced Settings Should be able to see, but not save global settings. ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../mount_management_section.tsx | 11 ++++-- .../public/management_app/settings.test.tsx | 37 ++++++++++++++++--- .../public/management_app/settings.tsx | 15 +++++--- .../advanced_settings_security.ts | 2 + 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx index 7372404f2cc8..0e65e1b67b01 100644 --- a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx +++ b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx @@ -63,10 +63,12 @@ export async function mountManagementSection( params.setBreadcrumbs(crumb); const [{ settings, notifications, docLinks, application, chrome }] = await getStartServices(); - const canSave = application.capabilities.advancedSettings.save as boolean; + const { advancedSettings, globalSettings } = application.capabilities; + const canSaveAdvancedSettings = advancedSettings.save as boolean; + const canSaveGlobalSettings = globalSettings.save as boolean; + const canShowGlobalSettings = globalSettings.show as boolean; const trackUiMetric = usageCollection?.reportUiCounter.bind(usageCollection, 'advanced_settings'); - - if (!canSave) { + if (!canSaveAdvancedSettings || (!canSaveGlobalSettings && canShowGlobalSettings)) { chrome.setBadge(readOnlyBadge); } @@ -82,7 +84,8 @@ export async function mountManagementSection( ({ Field: () => { @@ -251,7 +252,8 @@ describe('Settings', () => { const component = mountWithI18nProvider( { ).toHaveLength(1); }); - it('should should not render a custom setting', async () => { + it('should not render a custom setting', async () => { // The manual mock for the uiSettings client returns false for isConfig, override that const uiSettings = mockConfig().core.settings.client; uiSettings.isCustom = (key) => true; @@ -279,7 +281,8 @@ describe('Settings', () => { const component = mountWithI18nProvider( { const component = mountWithI18nProvider( { const component = mountWithI18nProvider( { expect(toasts.addWarning).toHaveBeenCalledTimes(1); expect(component.find(Search).prop('query').text).toEqual(''); }); + + it('does not render global settings if show is set to false', async () => { + const badQuery = 'category:(accessibility))'; + mockQuery(badQuery); + const { toasts } = notificationServiceMock.createStartContract(); + + const component = mountWithI18nProvider( + + ); + + expect(component.find(EuiTab).length).toEqual(1); + expect(component.find(EuiTab).at(0).text()).toEqual('Space Settings'); + }); }); diff --git a/src/plugins/advanced_settings/public/management_app/settings.tsx b/src/plugins/advanced_settings/public/management_app/settings.tsx index 9cf81ffff7fe..0a091aa55a48 100644 --- a/src/plugins/advanced_settings/public/management_app/settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/settings.tsx @@ -45,7 +45,8 @@ export type GroupedSettings = Record; interface Props { history: ScopedHistory; - enableSaving: boolean; + enableSaving: Record; + enableShowing: Record; settingsService: SettingsStart; docLinks: DocLinksStart['links']; toasts: ToastsStart; @@ -58,7 +59,8 @@ const SPACE_SETTINGS_ID = 'space-settings'; const GLOBAL_SETTINGS_ID = 'global-settings'; export const Settings = (props: Props) => { - const { componentRegistry, history, settingsService, ...rest } = props; + const { componentRegistry, history, settingsService, enableSaving, enableShowing, ...rest } = + props; const uiSettings = settingsService.client; const globalUiSettings = settingsService.globalClient; @@ -210,6 +212,7 @@ export const Settings = (props: Props) => { callOutSubtitle={callOutSubtitle(scope)} settingsService={settingsService} uiSettingsClient={getClientForScope(scope)} + enableSaving={enableSaving[scope]} {...rest} /> ); @@ -227,7 +230,9 @@ export const Settings = (props: Props) => { ) : null, content: renderAdvancedSettings('namespace'), }, - { + ]; + if (enableShowing.global) { + tabs.push({ id: GLOBAL_SETTINGS_ID, name: i18nTexts.globalTabTitle, append: @@ -238,8 +243,8 @@ export const Settings = (props: Props) => { ) : null, content: renderAdvancedSettings('global'), - }, - ]; + }); + } const [selectedTabId, setSelectedTabId] = useState(SPACE_SETTINGS_ID); diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index fb6f34aa24a1..eb5e34ce0207 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -27,6 +27,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { { feature: { advancedSettings: ['all'], + globalSettings: ['all'], }, spaces: ['*'], }, @@ -87,6 +88,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { { feature: { advancedSettings: ['read'], + globalSettings: ['show'], }, spaces: ['*'], }, From 58b36366cae363a64697df7d2e131fbc919af899 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Wed, 22 Mar 2023 21:09:22 -0400 Subject: [PATCH 3/3] [RAM] Fix bulk edit references (#153370) ## Summary Fix: https://github.com/elastic/kibana/issues/152961 https://github.com/elastic/kibana/issues/152960 https://github.com/elastic/kibana/issues/153175 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/rules_client/methods/bulk_edit.ts | 11 ++- .../group3/tests/alerting/bulk_edit.ts | 78 +++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts index 4bbe493d93c6..35c46ace4440 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts @@ -46,6 +46,7 @@ import { getBulkSnoozeAttributes, getBulkUnsnoozeAttributes, verifySnoozeScheduleLimit, + injectReferencesIntoParams, } from '../common'; import { alertingAuthorizationFilterOpts, @@ -435,10 +436,16 @@ async function updateRuleAttributesAndParamsInMemory( + rule.id, + ruleType, + attributes.params, + rule.references || [] + ); const { modifiedParams: ruleParams, isParamsUpdateSkipped } = paramsModifier - ? await paramsModifier(attributes.params as Params) + ? await paramsModifier(params) : { - modifiedParams: attributes.params as Params, + modifiedParams: params, isParamsUpdateSkipped: true, }; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts index 515b5662246b..2f3f36b9dc34 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_edit.ts @@ -610,5 +610,83 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { }); }); } + + describe('do NOT delete reference for rule type like', () => { + const es = getService('es'); + + it('.esquery', async () => { + const space1 = UserAtSpaceScenarios[1].space.id; + const { body: createdRule } = await supertest + .post(`${getUrlPrefix(space1)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + params: { + searchConfiguration: { + query: { query: 'host.name:*', language: 'kuery' }, + index: 'logs-*', + }, + timeField: '@timestamp', + searchType: 'searchSource', + timeWindowSize: 5, + timeWindowUnit: 'm', + threshold: [1000], + thresholdComparator: '>', + size: 100, + aggType: 'count', + groupBy: 'all', + termSize: 5, + excludeHitsFromPreviousRun: true, + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: [], + name: 'Es Query', + rule_type_id: '.es-query', + actions: [], + }) + ) + .expect(200); + objectRemover.add(space1, createdRule.id, 'rule', 'alerting'); + + const searchRule = () => + es.search<{ references: unknown }>({ + index: '.kibana*', + query: { + bool: { + filter: [ + { + term: { + _id: `alert:${createdRule.id}`, + }, + }, + ], + }, + }, + fields: ['alert.params', 'references'], + }); + + const { + hits: { hits: alertHitsV1 }, + } = await searchRule(); + + await supertest + .post(`${getUrlPrefix(space1)}/internal/alerting/rules/_bulk_edit`) + .set('kbn-xsrf', 'foo') + .send({ + ids: [createdRule.id], + operations: [{ operation: 'set', field: 'apiKey' }], + }); + + const { + hits: { hits: alertHitsV2 }, + } = await searchRule(); + + expect(alertHitsV1[0].fields).to.eql(alertHitsV2[0].fields); + expect(alertHitsV1[0]?._source?.references ?? true).to.eql( + alertHitsV2[0]?._source?.references ?? false + ); + }); + }); }); }