Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { AlertConsumers } from '@kbn/rule-data-utils';
import { APP_ID } from './application';
import type { Owner } from './types';
import type { ServerlessProjectType, Owner } from './types';

/**
* Owner
Expand All @@ -16,7 +16,14 @@ export const SECURITY_SOLUTION_OWNER = 'securitySolution' as const;
export const OBSERVABILITY_OWNER = 'observability' as const;
export const GENERAL_CASES_OWNER = APP_ID;

export const SECURITY_PROJECT_TYPE_ID = 'security';
export const OBSERVABILITY_PROJECT_TYPE_ID = 'observability';

export const OWNERS = [GENERAL_CASES_OWNER, OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER] as const;
export const SERVERLESS_PROJECT_TYPES = [
SECURITY_PROJECT_TYPE_ID,
OBSERVABILITY_PROJECT_TYPE_ID,
] as const;

interface RouteInfo {
id: Owner;
Expand All @@ -25,6 +32,7 @@ interface RouteInfo {
iconType: string;
appRoute: string;
validRuleConsumers?: readonly AlertConsumers[];
serverlessProjectType?: ServerlessProjectType;
}

export const OWNER_INFO: Record<Owner, RouteInfo> = {
Expand All @@ -35,6 +43,7 @@ export const OWNER_INFO: Record<Owner, RouteInfo> = {
iconType: 'logoSecurity',
appRoute: '/app/security',
validRuleConsumers: [AlertConsumers.SIEM],
serverlessProjectType: SECURITY_PROJECT_TYPE_ID,
},
[OBSERVABILITY_OWNER]: {
id: OBSERVABILITY_OWNER,
Expand All @@ -53,6 +62,7 @@ export const OWNER_INFO: Record<Owner, RouteInfo> = {
AlertConsumers.MONITORING,
AlertConsumers.STREAMS,
],
serverlessProjectType: OBSERVABILITY_PROJECT_TYPE_ID,
},
[GENERAL_CASES_OWNER]: {
id: GENERAL_CASES_OWNER,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import type { OWNERS } from './owners';
import type { SERVERLESS_PROJECT_TYPES, OWNERS } from './owners';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Interesting how this does not create circular dependencies. It also seems that this is only used to construct the types. Could we just create the union manually and add es and chat to the list?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I prefer this to be done in a follow-up PR, I'll create the GH issue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Opening an issue only for adding two variables seems like an overkill. Either we do it know or we do not do it at all. Up to you!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I wouldn't know how to fill everything in

  [SECURITY_SOLUTION_OWNER]: {
    id: SECURITY_SOLUTION_OWNER,
    appId: 'securitySolutionUI',
    label: 'Security',
    iconType: 'logoSecurity',
    appRoute: '/app/security',
    validRuleConsumers: [AlertConsumers.SIEM],
    serverlessProjectType: SECURITY_PROJECT_TYPE_ID,
  },

for es and chat. Maybe you are talking about something else?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see what you mean. I was only suggested to add them in SERVERLESS_PROJECT_TYPES. Not to fill the OWNER_INFO with the extra information. But you have a point, better to leave it as it is.


export enum HttpApiPrivilegeOperation {
Read = 'Read',
Expand All @@ -14,3 +14,4 @@ export enum HttpApiPrivilegeOperation {
}

export type Owner = (typeof OWNERS)[number];
export type ServerlessProjectType = (typeof SERVERLESS_PROJECT_TYPES)[number];
33 changes: 23 additions & 10 deletions x-pack/platform/plugins/shared/cases/common/utils/owner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { AlertConsumers } from '@kbn/rule-data-utils';
import { OWNER_INFO } from '../constants';
import { getCaseOwnerByAppId, getOwnerFromRuleConsumerProducer, isValidOwner } from './owner';
import type { ServerlessProjectType } from '../constants/types';

describe('owner utils', () => {
describe('isValidOwner', () => {
Expand Down Expand Up @@ -71,16 +72,6 @@ describe('owner utils', () => {
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);
});

it('fallbacks to producer when the consumer is alerts', () => {
const owner = getOwnerFromRuleConsumerProducer({
consumer: AlertConsumers.ALERTS,
Expand All @@ -89,5 +80,27 @@ describe('owner utils', () => {

expect(owner).toBe(OWNER_INFO.observability.id);
});

describe('serverless projects', () => {
const cloudProjects: Array<[ServerlessProjectType, string]> = [
[OWNER_INFO.observability.serverlessProjectType!, OWNER_INFO.observability.id],
[OWNER_INFO.securitySolution.serverlessProjectType!, OWNER_INFO.securitySolution.id],
// @ts-expect-error - we need to test the unknown project type
['unknown-by-us', OWNER_INFO.cases.id],
];

it.each(cloudProjects)(
'when the project type is %j, the owner should be %j',
(cloudProjectType, expectedOwner) => {
const owner = getOwnerFromRuleConsumerProducer({
consumer: 'should be ignored',
producer: 'should be ignored',
serverlessProjectType: cloudProjectType,
});

expect(owner).toBe(expectedOwner);
}
);
});
});
});
15 changes: 10 additions & 5 deletions x-pack/platform/plugins/shared/cases/common/utils/owner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { AlertConsumers } from '@kbn/rule-data-utils';
import { OWNER_INFO } from '../constants';
import type { Owner } from '../constants/types';
import type { ServerlessProjectType, Owner } from '../constants/types';

export const isValidOwner = (owner: string): owner is keyof typeof OWNER_INFO =>
Object.keys(OWNER_INFO).includes(owner);
Expand All @@ -18,16 +18,21 @@ export const getCaseOwnerByAppId = (currentAppId?: string) =>
export const getOwnerFromRuleConsumerProducer = ({
consumer,
producer,
isServerlessSecurity,
serverlessProjectType,
}: {
consumer?: string;
producer?: string;
isServerlessSecurity?: boolean;
serverlessProjectType?: ServerlessProjectType;
}): Owner => {
// This is a workaround for a very specific bug with the cases action in serverless security
// This same bug was later encountered in o11y as well
// More info here: https://github.com/elastic/kibana/issues/186270
if (isServerlessSecurity) {
return OWNER_INFO.securitySolution.id;
if (serverlessProjectType) {
const foundOwner = Object.entries(OWNER_INFO).find(([, info]) => {
return info.serverlessProjectType === serverlessProjectType;
});

return foundOwner ? foundOwner[1].id : OWNER_INFO.cases.id;
}

// Fallback to producer if the consumer is alerts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,54 @@ describe('CasesParamsFields renders', () => {
getConfigurationByOwnerSpy.mockRestore();
});

it('renders observability templates if the project is serverless observability', async () => {
useKibanaMock.mockReturnValue({
services: {
...createStartServicesMock(),
// simulate a observability security project
cloud: { isServerlessEnabled: true, serverless: { projectType: 'observability' } },
data: { dataViews: {} },
},
} as unknown as ReturnType<typeof useKibana>);

const configuration = {
...useGetAllCaseConfigurationsResponse.data[0],
templates: templatesConfigurationMock,
};
useGetAllCaseConfigurationsMock.mockImplementation(() => ({
...useGetAllCaseConfigurationsResponse,
data: [configuration],
}));
const getConfigurationByOwnerSpy = jest
.spyOn(utils, 'getConfigurationByOwner')
.mockImplementation(() => configuration);

const securityOwnedRule = {
...defaultProps,
// these two would normally produce a security owner
producerId: 'securitySolution',
featureId: 'securitySolution',
actionParams: {
subAction: 'run',
subActionParams: {
...actionParams.subActionParams,
templateId: templatesConfigurationMock[1].key,
},
},
};

render(<CasesParamsFields {...securityOwnedRule} />);

expect(getConfigurationByOwnerSpy).toHaveBeenCalledWith(
expect.objectContaining({
// the observability owner was forced
owner: 'observability',
})
);

getConfigurationByOwnerSpy.mockRestore();
});

it('updates template correctly', async () => {
useGetAllCaseConfigurationsMock.mockReturnValueOnce({
...useGetAllCaseConfigurationsResponse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '@elastic/eui';
import { useAlertsDataView } from '@kbn/alerts-ui-shared/src/common/hooks/use_alerts_data_view';
import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common';
import type { ServerlessProjectType } from '../../../../common/constants/types';
import * as i18n from './translations';
import type { CasesActionParams } from './types';
import { CASES_CONNECTOR_SUB_ACTION } from '../../../../common/constants';
Expand All @@ -48,13 +49,16 @@ export const CasesParamsFieldsComponent: React.FunctionComponent<
notifications: { toasts },
} = useKibana().services;

const serverlessProjectType = cloud?.isServerlessEnabled
? (cloud.serverless.projectType as ServerlessProjectType)
: undefined;

const owner = getOwnerFromRuleConsumerProducer({
consumer: featureId,
producer: producerId,
// 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/195599
isServerlessSecurity:
cloud?.isServerlessEnabled && cloud?.serverless.projectType === 'security',
serverlessProjectType,
});

const { dataView, isLoading: loadingAlertDataViews } = useAlertsDataView({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { SubActionConnectorType } from '@kbn/actions-plugin/server/sub_acti
import type { CasesConnectorConfig, CasesConnectorSecrets } from './types';
import { getCasesConnectorAdapter, getCasesConnectorType } from '.';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { OBSERVABILITY_PROJECT_TYPE_ID, SECURITY_PROJECT_TYPE_ID } from '../../../common/constants';
import { loggingSystemMock } from '@kbn/core/server/mocks';
import type { Logger } from '@kbn/core/server';
import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common';
Expand Down Expand Up @@ -340,7 +341,7 @@ describe('getCasesConnectorType', () => {

it('correctly fallsback to security owner if the project is serverless security', () => {
const adapter = getCasesConnectorAdapter({
isServerlessSecurity: true,
serverlessProjectType: SECURITY_PROJECT_TYPE_ID,
logger: mockLogger,
});

Expand Down Expand Up @@ -623,7 +624,7 @@ describe('getCasesConnectorType', () => {

it('correctly overrides the consumer and producer if the project is serverless security', () => {
const adapter = getCasesConnectorAdapter({
isServerlessSecurity: true,
serverlessProjectType: SECURITY_PROJECT_TYPE_ID,
logger: mockLogger,
});

Expand All @@ -645,6 +646,31 @@ describe('getCasesConnectorType', () => {
'cases:securitySolution/assignCase',
]);
});

it('correctly overrides the consumer and producer if the project is serverless observability', () => {
const adapter = getCasesConnectorAdapter({
serverlessProjectType: OBSERVABILITY_PROJECT_TYPE_ID,
logger: mockLogger,
});

expect(
adapter.getKibanaPrivileges?.({
consumer: 'alerts',
producer: AlertConsumers.SIEM,
})
).toEqual([
'cases:observability/createCase',
'cases:observability/updateCase',
'cases:observability/deleteCase',
'cases:observability/pushCase',
'cases:observability/createComment',
'cases:observability/updateComment',
'cases:observability/deleteComment',
'cases:observability/findConfigurations',
'cases:observability/reopenCase',
'cases:observability/assignCase',
]);
});
});
});
});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you please add a similar test, such as correctly overrides the consumer and producer if the project is serverless security, for observability?

Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@ import type { KibanaRequest } from '@kbn/core-http-server';
import type { Logger, SavedObjectsClientContract } from '@kbn/core/server';
import type { ConnectorAdapter } from '@kbn/alerting-plugin/server';
import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common';
import type { ServerlessProjectType } from '../../../common/constants/types';
import { CasesConnector } from './cases_connector';
import { DEFAULT_MAX_OPEN_CASES } from './constants';
import {
CASES_CONNECTOR_ID,
CASES_CONNECTOR_TITLE,
SECURITY_SOLUTION_OWNER,
} from '../../../common/constants';
import { CASES_CONNECTOR_ID, CASES_CONNECTOR_TITLE, OWNER_INFO } from '../../../common/constants';
import { getOwnerFromRuleConsumerProducer } from '../../../common/utils/owner';

import type {
Expand All @@ -47,14 +44,14 @@ interface GetCasesConnectorTypeArgs {
savedObjectTypes: string[]
) => Promise<SavedObjectsClientContract>;
getSpaceId: (request?: KibanaRequest) => string;
isServerlessSecurity?: boolean;
serverlessProjectType?: string;
}

export const getCasesConnectorType = ({
getCasesClient,
getSpaceId,
getUnsecuredSavedObjectsClient,
isServerlessSecurity,
serverlessProjectType,
}: GetCasesConnectorTypeArgs): SubActionConnectorType<
CasesConnectorConfig,
CasesConnectorSecrets
Expand Down Expand Up @@ -82,18 +79,26 @@ export const getCasesConnectorType = ({
throw new Error('Cannot authorize cases. Owner is not defined in the subActionParams.');
}

const owner = isServerlessSecurity
? SECURITY_SOLUTION_OWNER
: (params?.subActionParams?.owner as string);
let owner: string;
if (serverlessProjectType) {
const foundOwner = Object.entries(OWNER_INFO).find(([, info]) => {
return info.serverlessProjectType === serverlessProjectType;
});

owner = foundOwner ? foundOwner[1].id : OWNER_INFO.cases.id;
} else {
owner = params?.subActionParams?.owner as string;
}

return constructRequiredKibanaPrivileges(owner);
},
});

export const getCasesConnectorAdapter = ({
isServerlessSecurity,
serverlessProjectType,
logger,
}: {
serverlessProjectType?: ServerlessProjectType;
isServerlessSecurity?: boolean;
logger: Logger;
}): ConnectorAdapter<CasesConnectorRuleActionParams, CasesConnectorParams> => {
Expand Down Expand Up @@ -125,7 +130,7 @@ export const getCasesConnectorAdapter = ({
const owner = getOwnerFromRuleConsumerProducer({
consumer: rule.consumer,
producer: rule.producer,
isServerlessSecurity,
serverlessProjectType,
});

const subActionParams = {
Expand All @@ -144,7 +149,7 @@ export const getCasesConnectorAdapter = ({
return { subAction: 'run', subActionParams };
},
getKibanaPrivileges: ({ consumer, producer }) => {
const owner = getOwnerFromRuleConsumerProducer({ consumer, producer, isServerlessSecurity });
const owner = getOwnerFromRuleConsumerProducer({ consumer, producer, serverlessProjectType });
return constructRequiredKibanaPrivileges(owner);
},
};
Expand Down
Loading