diff --git a/x-pack/plugins/cases/common/utils/owner.test.ts b/x-pack/plugins/cases/common/utils/owner.test.ts index d3de319754725..a6f7c99f540bb 100644 --- a/x-pack/plugins/cases/common/utils/owner.test.ts +++ b/x-pack/plugins/cases/common/utils/owner.test.ts @@ -42,7 +42,7 @@ describe('owner utils', () => { it.each(owners)('returns owner %s correctly for consumer', (owner) => { for (const consumer of owner.validRuleConsumers ?? []) { - const result = getOwnerFromRuleConsumerProducer(consumer); + const result = getOwnerFromRuleConsumerProducer({ consumer }); expect(result).toBe(owner.id); } @@ -50,23 +50,33 @@ describe('owner utils', () => { it.each(owners)('returns owner %s correctly for producer', (owner) => { for (const producer of owner.validRuleConsumers ?? []) { - const result = getOwnerFromRuleConsumerProducer(undefined, producer); + const result = getOwnerFromRuleConsumerProducer({ producer }); expect(result).toBe(owner.id); } }); it('returns cases as a default owner', () => { - const owner = getOwnerFromRuleConsumerProducer(); + const owner = getOwnerFromRuleConsumerProducer({}); expect(owner).toBe(OWNER_INFO.cases.id); }); - it('returns owner as per consumer when both values are passed ', () => { - const owner = getOwnerFromRuleConsumerProducer( - AlertConsumers.SIEM, - AlertConsumers.OBSERVABILITY - ); + it('returns owner as per consumer when both values are passed', () => { + const owner = getOwnerFromRuleConsumerProducer({ + consumer: AlertConsumers.SIEM, + producer: AlertConsumers.OBSERVABILITY, + }); + + expect(owner).toBe(OWNER_INFO.securitySolution.id); + }); + + it('returns securitySolution owner if project isServerlessSecurity', () => { + const owner = getOwnerFromRuleConsumerProducer({ + consumer: AlertConsumers.OBSERVABILITY, + producer: AlertConsumers.OBSERVABILITY, + isServerlessSecurity: true, + }); expect(owner).toBe(OWNER_INFO.securitySolution.id); }); diff --git a/x-pack/plugins/cases/common/utils/owner.ts b/x-pack/plugins/cases/common/utils/owner.ts index d3650f0995b86..7bde7220233db 100644 --- a/x-pack/plugins/cases/common/utils/owner.ts +++ b/x-pack/plugins/cases/common/utils/owner.ts @@ -14,7 +14,21 @@ export const isValidOwner = (owner: string): owner is keyof typeof OWNER_INFO => export const getCaseOwnerByAppId = (currentAppId?: string) => Object.values(OWNER_INFO).find((info) => info.appId === currentAppId)?.id; -export const getOwnerFromRuleConsumerProducer = (consumer?: string, producer?: string): Owner => { +export const getOwnerFromRuleConsumerProducer = ({ + consumer, + producer, + isServerlessSecurity, +}: { + consumer?: string; + producer?: string; + isServerlessSecurity?: boolean; +}): Owner => { + // This is a workaround for a very specific bug with the cases action in serverless security + // More info here: https://github.com/elastic/kibana/issues/186270 + if (isServerlessSecurity) { + return OWNER_INFO.securitySolution.id; + } + for (const value of Object.values(OWNER_INFO)) { const foundConsumer = value.validRuleConsumers?.find( (validConsumer) => validConsumer === consumer || validConsumer === producer diff --git a/x-pack/plugins/cases/kibana.jsonc b/x-pack/plugins/cases/kibana.jsonc index 84c04da1fe0f6..300b1ee4c2c12 100644 --- a/x-pack/plugins/cases/kibana.jsonc +++ b/x-pack/plugins/cases/kibana.jsonc @@ -30,6 +30,7 @@ "uiActions", ], "optionalPlugins": [ + "cloud", "home", "taskManager", "usageCollection", diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx b/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx index 6b6f85e53045f..6c93b2435af8e 100644 --- a/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx +++ b/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx @@ -43,7 +43,7 @@ export const CasesParamsFieldsComponent: React.FunctionComponent< notifications: { toasts }, data: { dataViews: dataViewsService }, } = useKibana().services; - const owner = getOwnerFromRuleConsumerProducer(featureId, producerId); + const owner = getOwnerFromRuleConsumerProducer({ consumer: featureId, producer: producerId }); const { dataView, isLoading: loadingAlertDataViews } = useAlertsDataView({ http, diff --git a/x-pack/plugins/cases/server/connectors/cases/index.test.ts b/x-pack/plugins/cases/server/connectors/cases/index.test.ts index cca7092fde368..5c7b29ef4e704 100644 --- a/x-pack/plugins/cases/server/connectors/cases/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/cases/index.test.ts @@ -80,26 +80,26 @@ describe('getCasesConnectorType', () => { }); it('sets the correct connectorTypeId', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect(adapter.connectorTypeId).toEqual('.cases'); }); describe('ruleActionParamsSchema', () => { it('validates getParams() correctly', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect(adapter.ruleActionParamsSchema.validate(getParams())).toEqual(getParams()); }); it('throws if missing getParams()', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect(() => adapter.ruleActionParamsSchema.validate({})).toThrow(); }); it('does not accept more than one groupingBy key', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect(() => adapter.ruleActionParamsSchema.validate( @@ -109,7 +109,7 @@ describe('getCasesConnectorType', () => { }); it('should fail with not valid time window', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect(() => adapter.ruleActionParamsSchema.validate(getParams({ timeWindow: '10d+3d' })) @@ -119,7 +119,7 @@ describe('getCasesConnectorType', () => { describe('buildActionParams', () => { it('builds the action getParams() correctly', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect( adapter.buildActionParams({ @@ -164,7 +164,7 @@ describe('getCasesConnectorType', () => { }); it('builds the action getParams() and templateId correctly', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect( adapter.buildActionParams({ @@ -209,7 +209,7 @@ describe('getCasesConnectorType', () => { }); it('builds the action getParams() correctly without ruleUrl', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect( adapter.buildActionParams({ // @ts-expect-error: not all fields are needed @@ -252,7 +252,7 @@ describe('getCasesConnectorType', () => { }); it('maps observability consumers to the correct owner', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); for (const consumer of [ AlertConsumers.OBSERVABILITY, @@ -276,7 +276,7 @@ describe('getCasesConnectorType', () => { }); it('maps security solution consumers to the correct owner', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); for (const consumer of [AlertConsumers.SIEM]) { const connectorParams = adapter.buildActionParams({ @@ -292,7 +292,7 @@ describe('getCasesConnectorType', () => { }); it('maps stack consumers to the correct owner', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); for (const consumer of [AlertConsumers.ML, AlertConsumers.STACK_ALERTS]) { const connectorParams = adapter.buildActionParams({ @@ -308,7 +308,7 @@ describe('getCasesConnectorType', () => { }); it('fallback to the cases owner if the consumer is not in the mapping', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); const connectorParams = adapter.buildActionParams({ // @ts-expect-error: not all fields are needed @@ -320,11 +320,27 @@ describe('getCasesConnectorType', () => { expect(connectorParams.subActionParams.owner).toBe('cases'); }); + + it('correctly fallsback to security owner if the project is serverless security', () => { + const adapter = getCasesConnectorAdapter({ isServerlessSecurity: true }); + + for (const consumer of [AlertConsumers.ML, AlertConsumers.STACK_ALERTS]) { + const connectorParams = adapter.buildActionParams({ + // @ts-expect-error: not all fields are needed + alerts, + rule: { ...rule, consumer }, + params: getParams(), + spaceId: 'default', + }); + + expect(connectorParams.subActionParams.owner).toBe('securitySolution'); + } + }); }); describe('getKibanaPrivileges', () => { it('constructs the correct privileges from the consumer', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect( adapter.getKibanaPrivileges?.({ @@ -344,7 +360,7 @@ describe('getCasesConnectorType', () => { }); it('constructs the correct privileges from the producer if the consumer is not found', () => { - const adapter = getCasesConnectorAdapter(); + const adapter = getCasesConnectorAdapter({}); expect( adapter.getKibanaPrivileges?.({ @@ -362,6 +378,26 @@ describe('getCasesConnectorType', () => { 'cases:observability/findConfigurations', ]); }); + + it('correctly overrides the consumer and producer if the project is serverless security', () => { + const adapter = getCasesConnectorAdapter({ isServerlessSecurity: true }); + + expect( + adapter.getKibanaPrivileges?.({ + consumer: 'alerting', + producer: AlertConsumers.LOGS, + }) + ).toEqual([ + 'cases:securitySolution/createCase', + 'cases:securitySolution/updateCase', + 'cases:securitySolution/deleteCase', + 'cases:securitySolution/pushCase', + 'cases:securitySolution/createComment', + 'cases:securitySolution/updateComment', + 'cases:securitySolution/deleteComment', + 'cases:securitySolution/findConfigurations', + ]); + }); }); }); }); diff --git a/x-pack/plugins/cases/server/connectors/cases/index.ts b/x-pack/plugins/cases/server/connectors/cases/index.ts index 8be0b645cbfb3..07b4ab5e29551 100644 --- a/x-pack/plugins/cases/server/connectors/cases/index.ts +++ b/x-pack/plugins/cases/server/connectors/cases/index.ts @@ -16,7 +16,11 @@ import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { ConnectorAdapter } from '@kbn/alerting-plugin/server'; import { CasesConnector } from './cases_connector'; import { DEFAULT_MAX_OPEN_CASES } from './constants'; -import { CASES_CONNECTOR_ID, CASES_CONNECTOR_TITLE } from '../../../common/constants'; +import { + CASES_CONNECTOR_ID, + CASES_CONNECTOR_TITLE, + SECURITY_SOLUTION_OWNER, +} from '../../../common/constants'; import { getOwnerFromRuleConsumerProducer } from '../../../common/utils/owner'; import type { @@ -40,12 +44,14 @@ interface GetCasesConnectorTypeArgs { savedObjectTypes: string[] ) => Promise; getSpaceId: (request?: KibanaRequest) => string; + isServerlessSecurity?: boolean; } export const getCasesConnectorType = ({ getCasesClient, getSpaceId, getUnsecuredSavedObjectsClient, + isServerlessSecurity, }: GetCasesConnectorTypeArgs): SubActionConnectorType< CasesConnectorConfig, CasesConnectorSecrets @@ -69,27 +75,34 @@ export const getCasesConnectorType = ({ minimumLicenseRequired: 'platinum' as const, isSystemActionType: true, getKibanaPrivileges: ({ params } = { params: { subAction: 'run', subActionParams: {} } }) => { - const owner = params?.subActionParams?.owner as string; - - if (!owner) { + if (!params?.subActionParams?.owner) { throw new Error('Cannot authorize cases. Owner is not defined in the subActionParams.'); } + const owner = isServerlessSecurity + ? SECURITY_SOLUTION_OWNER + : (params?.subActionParams?.owner as string); + return constructRequiredKibanaPrivileges(owner); }, }); -export const getCasesConnectorAdapter = (): ConnectorAdapter< - CasesConnectorRuleActionParams, - CasesConnectorParams -> => { +export const getCasesConnectorAdapter = ({ + isServerlessSecurity, +}: { + isServerlessSecurity?: boolean; +}): ConnectorAdapter => { return { connectorTypeId: CASES_CONNECTOR_ID, ruleActionParamsSchema: CasesConnectorRuleActionParamsSchema, buildActionParams: ({ alerts, rule, params, spaceId, ruleUrl }) => { const caseAlerts = [...alerts.new.data, ...alerts.ongoing.data]; - const owner = getOwnerFromRuleConsumerProducer(rule.consumer, rule.producer); + const owner = getOwnerFromRuleConsumerProducer({ + consumer: rule.consumer, + producer: rule.producer, + isServerlessSecurity, + }); const subActionParams = { alerts: caseAlerts, @@ -105,7 +118,7 @@ export const getCasesConnectorAdapter = (): ConnectorAdapter< return { subAction: 'run', subActionParams }; }, getKibanaPrivileges: ({ consumer, producer }) => { - const owner = getOwnerFromRuleConsumerProducer(consumer, producer); + const owner = getOwnerFromRuleConsumerProducer({ consumer, producer, isServerlessSecurity }); return constructRequiredKibanaPrivileges(owner); }, }; diff --git a/x-pack/plugins/cases/server/connectors/index.ts b/x-pack/plugins/cases/server/connectors/index.ts index 2d680163dde28..0b0f201b46d42 100644 --- a/x-pack/plugins/cases/server/connectors/index.ts +++ b/x-pack/plugins/cases/server/connectors/index.ts @@ -22,12 +22,14 @@ export function registerConnectorTypes({ core, getCasesClient, getSpaceId, + isServerlessSecurity, }: { actions: ActionsPluginSetupContract; alerting: AlertingPluginSetup; core: CoreSetup; getCasesClient: (request: KibanaRequest) => Promise; getSpaceId: (request?: KibanaRequest) => string; + isServerlessSecurity?: boolean; }) { const getUnsecuredSavedObjectsClient = async ( request: KibanaRequest, @@ -53,8 +55,13 @@ export function registerConnectorTypes({ }; actions.registerSubActionConnectorType( - getCasesConnectorType({ getCasesClient, getSpaceId, getUnsecuredSavedObjectsClient }) + getCasesConnectorType({ + getCasesClient, + getSpaceId, + getUnsecuredSavedObjectsClient, + isServerlessSecurity, + }) ); - alerting.registerConnectorAdapter(getCasesConnectorAdapter()); + alerting.registerConnectorAdapter(getCasesConnectorAdapter({ isServerlessSecurity })); } diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 262289bc6a24c..fa172b48520a7 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -145,12 +145,16 @@ export class CasePlugin return plugins.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; }; + const isServerlessSecurity = + plugins.cloud?.isServerlessEnabled && plugins.cloud?.serverless.projectType === 'security'; + registerConnectorTypes({ actions: plugins.actions, alerting: plugins.alerting, core, getCasesClient, getSpaceId, + isServerlessSecurity, }); return { diff --git a/x-pack/plugins/cases/server/types.ts b/x-pack/plugins/cases/server/types.ts index 0153083337cfa..a51817c9d7e58 100644 --- a/x-pack/plugins/cases/server/types.ts +++ b/x-pack/plugins/cases/server/types.ts @@ -30,6 +30,7 @@ import type { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing- import type { NotificationsPluginStart } from '@kbn/notifications-plugin/server'; import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server'; import type { PluginSetupContract as AlertingPluginSetup } from '@kbn/alerting-plugin/server'; +import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { CasesClient } from './client'; import type { AttachmentFramework } from './attachment_framework/types'; import type { ExternalReferenceAttachmentTypeRegistry } from './attachment_framework/external_reference_registry'; @@ -46,6 +47,7 @@ export interface CasesServerSetupDependencies { taskManager?: TaskManagerSetupContract; usageCollection?: UsageCollectionSetup; spaces?: SpacesPluginSetup; + cloud?: CloudSetup; } export interface CasesServerStartDependencies { diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index fbe05134b5bfd..1d126d78f9543 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -76,6 +76,7 @@ "@kbn/core-http-router-server-internal", "@kbn/presentation-publishing", "@kbn/alerts-ui-shared", + "@kbn/cloud-plugin", ], "exclude": [ "target/**/*",