Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ResponseOps][Cases] Fix case actions bug in serverless security #195281

Merged
merged 2 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am curious why we don't need check for o11y.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also curious. Stack cases are not available in o11y. What is the current behavior if you create a stack alert from the stack management page?

Copy link
Contributor Author

@adcoelho adcoelho Oct 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stack rules are not available in serverless observability. So this scenario won't happen.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot that, thanks!

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