Skip to content

Commit

Permalink
[ResponseOps][Cases] Fix case actions bug in serverless security (ela…
Browse files Browse the repository at this point in the history
…stic#195281)

Fixes elastic#186270

## Summary

This PR ensures that cases created by the case action in stack
management rules in serverless security projects are assigned the
correct owner.

### How to test

1. Add the following line to `serverless.yml` -
`xpack.cloud.serverless.project_id: test-123`
2. Start elastic search in serverless security mode - `yarn es
serverless --projectType security`
3. Start Kibana in serverless security mode - `yarn start
--serverless=security`
4. Go to stack and create a rule with the cases action.
5. When an alert is triggered confirm you can view the case in `Security
> Cases`

---------

Co-authored-by: kibanamachine <[email protected]>
(cherry picked from commit 02cc5a8)
  • Loading branch information
adcoelho committed Oct 9, 2024
1 parent deeb604 commit b6b59bf
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 36 deletions.
26 changes: 18 additions & 8 deletions x-pack/plugins/cases/common/utils/owner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,31 +42,41 @@ 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);
}
});

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);
});
Expand Down
16 changes: 15 additions & 1 deletion x-pack/plugins/cases/common/utils/owner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/cases/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"uiActions",
],
"optionalPlugins": [
"cloud",
"home",
"taskManager",
"usageCollection",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
64 changes: 50 additions & 14 deletions x-pack/plugins/cases/server/connectors/cases/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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' }))
Expand All @@ -119,7 +119,7 @@ describe('getCasesConnectorType', () => {

describe('buildActionParams', () => {
it('builds the action getParams() correctly', () => {
const adapter = getCasesConnectorAdapter();
const adapter = getCasesConnectorAdapter({});

expect(
adapter.buildActionParams({
Expand Down Expand Up @@ -164,7 +164,7 @@ describe('getCasesConnectorType', () => {
});

it('builds the action getParams() and templateId correctly', () => {
const adapter = getCasesConnectorAdapter();
const adapter = getCasesConnectorAdapter({});

expect(
adapter.buildActionParams({
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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({
Expand All @@ -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({
Expand All @@ -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
Expand All @@ -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?.({
Expand All @@ -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?.({
Expand All @@ -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',
]);
});
});
});
});
33 changes: 23 additions & 10 deletions x-pack/plugins/cases/server/connectors/cases/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -40,12 +44,14 @@ interface GetCasesConnectorTypeArgs {
savedObjectTypes: string[]
) => Promise<SavedObjectsClientContract>;
getSpaceId: (request?: KibanaRequest) => string;
isServerlessSecurity?: boolean;
}

export const getCasesConnectorType = ({
getCasesClient,
getSpaceId,
getUnsecuredSavedObjectsClient,
isServerlessSecurity,
}: GetCasesConnectorTypeArgs): SubActionConnectorType<
CasesConnectorConfig,
CasesConnectorSecrets
Expand All @@ -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<CasesConnectorRuleActionParams, CasesConnectorParams> => {
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,
Expand All @@ -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);
},
};
Expand Down
11 changes: 9 additions & 2 deletions x-pack/plugins/cases/server/connectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ export function registerConnectorTypes({
core,
getCasesClient,
getSpaceId,
isServerlessSecurity,
}: {
actions: ActionsPluginSetupContract;
alerting: AlertingPluginSetup;
core: CoreSetup;
getCasesClient: (request: KibanaRequest) => Promise<CasesClient>;
getSpaceId: (request?: KibanaRequest) => string;
isServerlessSecurity?: boolean;
}) {
const getUnsecuredSavedObjectsClient = async (
request: KibanaRequest,
Expand All @@ -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 }));
}
4 changes: 4 additions & 0 deletions x-pack/plugins/cases/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit b6b59bf

Please sign in to comment.