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 7372404f2cc80..0e65e1b67b012 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 9cf81ffff7fe0..0a091aa55a481 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/plugins/alerting/server/rules_client/methods/bulk_edit.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts index 4bbe493d93c6b..35c46ace44408 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/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 39a1cbf2fd320..04e31f9e0b641 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 dc229560b502a..83fd9a4b276c1 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 7c5d4eb8dc31b..31edc058dec52 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 749aa1ccb038a..4d6ec61ea692b 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 8d4cf6773dfc7..a697b12190122 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 f8f2c9dc41e97..8109baf738a18 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 b13deea527463..bf98aff994bc3 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 be2f5872a2dac..346ed041520de 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 09983e7a6cc66..e82a38e006ce9 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 5840a38d94ef3..118cabee95181 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 984c68a76a6b6..c8d876a7281da 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 1b66a4e401311..2696e44c8c025 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 d8b432bb0391e..210e60919fbca 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 cd019c189c76f..6012583104f35 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 98c6536c277d3..7f1345ced245f 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 eead1dc267e60..23578194422ee 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 60c540b422bcd..a0fe377baf69c 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/plugins/enterprise_search/server/lib/engines/create_api_key.test.ts b/x-pack/plugins/enterprise_search/server/lib/engines/create_api_key.test.ts index ca076fe436248..474a1c84e16fa 100644 --- a/x-pack/plugins/enterprise_search/server/lib/engines/create_api_key.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/engines/create_api_key.test.ts @@ -11,7 +11,7 @@ import { createApiKey } from './create_api_key'; describe('createApiKey lib function', () => { const engineName = 'my-index'; - const keyName = 'Engine read only key'; + const keyName = 'Search alias read only key'; const createResponse = { api_key: 'ui2lp2axTNmsyakw9tvNnw', @@ -38,14 +38,14 @@ describe('createApiKey lib function', () => { ).resolves.toEqual(createResponse); expect(mockClient.asCurrentUser.security.createApiKey).toHaveBeenCalledWith({ - name: 'Engine read only key', + name: 'Search alias read only key', role_descriptors: { 'my-index-key-role': { - applications: [ + cluster: [], + indices: [ { - application: 'enterprise-search', - privileges: ['engine:read'], - resources: ['engine:my-index'], + names: [`${engineName}`], + privileges: ['read'], }, ], }, diff --git a/x-pack/plugins/enterprise_search/server/lib/engines/create_api_key.ts b/x-pack/plugins/enterprise_search/server/lib/engines/create_api_key.ts index 508ed6d56bd59..8c8c780dc461d 100644 --- a/x-pack/plugins/enterprise_search/server/lib/engines/create_api_key.ts +++ b/x-pack/plugins/enterprise_search/server/lib/engines/create_api_key.ts @@ -16,11 +16,11 @@ export const createApiKey = async ( name: keyName, role_descriptors: { [`${engineName}-key-role`]: { - applications: [ + cluster: [], + indices: [ { - application: 'enterprise-search', - privileges: ['engine:read'], - resources: [`engine:${engineName}`], + names: [`${engineName}`], + privileges: ['read'], }, ], }, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_diagnostics/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_diagnostics/index.tsx index de3600142d8ca..c52eddd3cafb9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_diagnostics/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_diagnostics/index.tsx @@ -249,7 +249,7 @@ export const AgentDiagnosticsTab: React.FunctionComponent > diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_request_diagnostics_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_request_diagnostics_modal/index.tsx index d5bb9adf040b2..0d3ccbd804e52 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_request_diagnostics_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_request_diagnostics_modal/index.tsx @@ -119,7 +119,7 @@ export const AgentRequestDiagnosticsModal: React.FunctionComponent = ({

diff --git a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.test.ts b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.test.ts index b342b52cdb9bb..efe22f0111c63 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.test.ts @@ -57,8 +57,8 @@ describe('getDocumentPayload', () => { expect.objectContaining({ contentType: 'application/pdf', content: expect.any(Readable), + filename: 'Some PDF report.pdf', headers: expect.objectContaining({ - 'Content-Disposition': 'attachment; filename="Some PDF report.pdf"', 'Content-Length': '1024', }), statusCode: 200, @@ -85,8 +85,8 @@ describe('getDocumentPayload', () => { expect.objectContaining({ contentType: 'text/csv', content: expect.any(Readable), + filename: 'Some CSV report.csv', headers: expect.objectContaining({ - 'Content-Disposition': 'attachment; filename="Some CSV report.csv"', 'Content-Length': '1024', 'kbn-csv-contains-formulas': true, 'kbn-max-size-reached': true, diff --git a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts index 52dec4467a7a0..58f878e7a29d7 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -24,8 +24,11 @@ export interface Payload { content: string | Stream | ErrorFromPayload; contentType: string | null; headers: ResponseHeaders; + filename?: string; } +export type PayloadCompleted = Payload & { filename: string }; + type TaskRunResult = Required['output']; const DEFAULT_TITLE = 'report'; @@ -67,12 +70,12 @@ export function getDocumentPayloadFactory(reporting: ReportingCore) { const contentType = output.content_type ?? 'text/plain'; return { + filename, content, statusCode: 200, contentType, headers: { ...headers, - 'Content-Disposition': `attachment; filename="${filename}"`, 'Content-Length': `${output.size ?? ''}`, }, }; diff --git a/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts b/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts index 9f6e3e0fe235b..e5b326df56f56 100644 --- a/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts +++ b/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts @@ -8,13 +8,14 @@ jest.mock('../../../lib/content_stream', () => ({ getContentStream: jest.fn(), })); +import { estypes } from '@elastic/elasticsearch'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; import type { ElasticsearchClientMock } from '@kbn/core/server/mocks'; +import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; import { BehaviorSubject } from 'rxjs'; -import { setupServer } from '@kbn/core-test-helpers-test-utils'; import { Readable } from 'stream'; import supertest from 'supertest'; import { ReportingCore } from '../../..'; -import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; import { ReportingInternalSetup, ReportingInternalStart } from '../../../core'; import { ContentStream, ExportTypesRegistry, getContentStream } from '../../../lib'; import { @@ -44,7 +45,7 @@ describe('GET /api/reporting/jobs/download', () => { hits: { hits: sources.map((source: object) => ({ _source: source })), }, - }; + } as estypes.SearchResponseBody; }; const mockConfigSchema = createMockConfigSchema({ roles: { enabled: false } }); @@ -113,7 +114,7 @@ describe('GET /api/reporting/jobs/download', () => { }); it('fails on malformed download IDs', async () => { - mockEsClient.search.mockResponseOnce(getHits() as any); + mockEsClient.search.mockResponseOnce(getHits()); registerJobInfoRoutes(core); await server.start(); @@ -153,7 +154,7 @@ describe('GET /api/reporting/jobs/download', () => { }); it('returns 404 if job not found', async () => { - mockEsClient.search.mockResponseOnce(getHits() as any); + mockEsClient.search.mockResponseOnce(getHits()); registerJobInfoRoutes(core); await server.start(); @@ -166,7 +167,7 @@ describe('GET /api/reporting/jobs/download', () => { getHits({ jobtype: 'invalidJobType', payload: { title: 'invalid!' }, - }) as any + }) ); registerJobInfoRoutes(core); @@ -180,7 +181,7 @@ describe('GET /api/reporting/jobs/download', () => { getHits({ jobtype: 'base64EncodedJobType', payload: {}, // payload is irrelevant - }) as any + }) ); registerJobInfoRoutes(core); @@ -195,7 +196,7 @@ describe('GET /api/reporting/jobs/download', () => { getHits({ jobtype: 'customForbiddenJobType', payload: {}, // payload is irrelevant - }) as any + }) ); registerJobInfoRoutes(core); @@ -211,7 +212,7 @@ describe('GET /api/reporting/jobs/download', () => { jobtype: 'unencodedJobType', status: 'pending', payload: { title: 'incomplete!' }, - }) as any + }) ); registerJobInfoRoutes(core); @@ -231,7 +232,7 @@ describe('GET /api/reporting/jobs/download', () => { status: 'failed', output: { content: 'job failure message' }, payload: { title: 'failing job!' }, - }) as any + }) ); registerJobInfoRoutes(core); @@ -256,23 +257,23 @@ describe('GET /api/reporting/jobs/download', () => { status: 'completed', output: { content_type: outputContentType }, payload: { title }, - }); + }) as estypes.SearchResponseBody; }; it('when a known job-type is complete', async () => { - mockEsClient.search.mockResponseOnce(getCompleteHits() as any); + mockEsClient.search.mockResponseOnce(getCompleteHits()); registerJobInfoRoutes(core); await server.start(); await supertest(httpSetup.server.listener) .get('/api/reporting/jobs/download/dank') .expect(200) - .expect('Content-Type', 'text/plain; charset=utf-8') - .expect('content-disposition', 'attachment; filename="report.csv"'); + .expect('Content-Type', 'text/csv; charset=utf-8') + .expect('content-disposition', 'attachment; filename=report.csv'); }); it('succeeds when security is not there or disabled', async () => { - mockEsClient.search.mockResponseOnce(getCompleteHits() as any); + mockEsClient.search.mockResponseOnce(getCompleteHits()); // @ts-ignore core.pluginSetupDeps.security = null; @@ -284,15 +285,15 @@ describe('GET /api/reporting/jobs/download', () => { await supertest(httpSetup.server.listener) .get('/api/reporting/jobs/download/dope') .expect(200) - .expect('Content-Type', 'text/plain; charset=utf-8') - .expect('content-disposition', 'attachment; filename="report.csv"'); + .expect('Content-Type', 'text/csv; charset=utf-8') + .expect('content-disposition', 'attachment; filename=report.csv'); }); it('forwards job content stream', async () => { mockEsClient.search.mockResponseOnce( getCompleteHits({ jobType: 'unencodedJobType', - }) as any + }) ); registerJobInfoRoutes(core); @@ -300,7 +301,7 @@ describe('GET /api/reporting/jobs/download', () => { await supertest(httpSetup.server.listener) .get('/api/reporting/jobs/download/dank') .expect(200) - .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Content-Type', 'text/csv; charset=utf-8') .then(({ text }) => expect(text).toEqual('test')); }); @@ -309,7 +310,7 @@ describe('GET /api/reporting/jobs/download', () => { getCompleteHits({ jobType: 'unencodedJobType', outputContentType: 'application/html', - }) as any + }) ); registerJobInfoRoutes(core); @@ -325,6 +326,26 @@ describe('GET /api/reporting/jobs/download', () => { }); }); }); + + it('allows multi-byte characters in file names', async () => { + mockEsClient.search.mockResponseOnce( + getCompleteHits({ + jobType: 'base64EncodedJobType', + title: '日本語ダッシュボード', + }) + ); + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/japanese-dashboard') + .expect(200) + .expect('Content-Type', 'application/pdf') + .expect( + 'content-disposition', + 'attachment; filename=%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%83%80%E3%83%83%E3%82%B7%E3%83%A5%E3%83%9C%E3%83%BC%E3%83%89.pdf' + ); + }); }); describe('Deprecated: role-based access control', () => { diff --git a/x-pack/plugins/reporting/server/routes/management/jobs.ts b/x-pack/plugins/reporting/server/routes/management/jobs.ts index 8c93f25e80873..9b2a63deca2ba 100644 --- a/x-pack/plugins/reporting/server/routes/management/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/management/jobs.ts @@ -165,22 +165,26 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { return jobManagementPreRouting(reporting, res, docId, user, counters, async (doc) => { const payload = await jobsQuery.getDocumentPayload(doc); + const { contentType, content, filename, statusCode } = payload; - if (!payload.contentType || !ALLOWED_JOB_CONTENT_TYPES.includes(payload.contentType)) { + if (!contentType || !ALLOWED_JOB_CONTENT_TYPES.includes(contentType)) { return res.badRequest({ - body: `Unsupported content-type of ${payload.contentType} specified by job output`, + body: `Unsupported content-type of ${contentType} specified by job output`, }); } - return res.custom({ - body: - typeof payload.content === 'string' ? Buffer.from(payload.content) : payload.content, - statusCode: payload.statusCode, - headers: { - ...payload.headers, - 'content-type': payload.contentType, - }, - }); + const body = typeof content === 'string' ? Buffer.from(content) : content; + + const headers = { + ...payload.headers, + 'content-type': contentType, + }; + + if (filename) { + return res.file({ body, headers, filename }); + } + + return res.custom({ body, headers, statusCode }); }); }) ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts index c0b50798a822c..16558869a7caf 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts @@ -61,7 +61,7 @@ describe('Pagination', () => { const builder = PaginationBuilder.createBuilder(100); expect(builder.buildQueryFields('a', 'desc').sort).toStrictEqual([ { '@timestamp': 'desc' }, - { a: { order: 'asc', unmapped_type: 'long' } }, + { a: { order: 'asc', unmapped_type: 'keyword' } }, ]); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts index 295d930ff24f5..19825ea0fd38d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts @@ -179,7 +179,7 @@ export class PaginationBuilder { ): PaginationFields { const sort: SortFields = [ { '@timestamp': timeSort }, - { [tiebreaker]: { order: 'asc', unmapped_type: 'long' } }, + { [tiebreaker]: { order: 'asc', unmapped_type: 'keyword' } }, ]; let searchAfter: SearchAfterFields | undefined; if (this.timestamp && this.eventID) { 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 515b5662246b7..2f3f36b9dc34b 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 + ); + }); + }); }); } 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 6d10aa2f60f4a..89ba33449ec16 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 6a1222ef1e099..010bb788b8c70 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( 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 fb6f34aa24a14..eb5e34ce0207a 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: ['*'], }, diff --git a/x-pack/test/functional/apps/canvas/reports.ts b/x-pack/test/functional/apps/canvas/reports.ts index b2c7444b44fd6..1a1550a62c8eb 100644 --- a/x-pack/test/functional/apps/canvas/reports.ts +++ b/x-pack/test/functional/apps/canvas/reports.ts @@ -64,7 +64,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(res.status).to.equal(200); expect(res.get('content-type')).to.equal('application/pdf'); expect(res.get('content-disposition')).to.equal( - 'attachment; filename="The Very Cool Workpad for PDF Tests.pdf"' + 'attachment; filename=The%20Very%20Cool%20Workpad%20for%20PDF%20Tests.pdf' ); }); }); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/csv_v2.snap b/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/csv_v2.snap index a094bad84d444..1d9f1ae4195d4 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/csv_v2.snap +++ b/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/csv_v2.snap @@ -13,7 +13,7 @@ u_JJX4UBvD7uFsw9L2x4,timeless-test,1,Proterozoic,-,Paleozoic,Permian exports[`Reporting APIs CSV Generation from Saved Search ID export from non-timebased data view query stored in the saved search job response data is correct 1`] = ` Object { - "contentDisposition": "attachment; filename=\\"*zoic.csv\\"", + "contentDisposition": "attachment; filename=*zoic.csv", "contentType": "text/csv; charset=utf-8", "title": "*zoic", } @@ -66,7 +66,7 @@ exports[`Reporting APIs CSV Generation from Saved Search ID export from timebase exports[`Reporting APIs CSV Generation from Saved Search ID export from timebased data view timezone formatting export with custom timezone and timeRange from locator params job response data is correct 1`] = ` Object { - "contentDisposition": "attachment; filename=\\"Ecommerce Data.csv\\"", + "contentDisposition": "attachment; filename=Ecommerce%20Data.csv", "contentType": "text/csv; charset=utf-8", "title": "Ecommerce Data", }