From d5bbd8e8da50a43d77bc15bab77d6ad7e87dbc42 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 23 Oct 2023 20:13:05 +0300 Subject: [PATCH] [Cases] Case action: Registration and Oracle (#168370) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../current_mappings.json | 28 ++ .../group2/check_registered_types.test.ts | 1 + .../group3/type_registrations.test.ts | 1 + .../group5/dot_kibana_split.test.ts | 1 + .../plugins/cases/common/constants/index.ts | 1 + .../connectors/cases/cases_connector.ts | 42 +++ .../cases/cases_oracle_service.test.ts | 283 ++++++++++++++++++ .../connectors/cases/cases_oracle_service.ts | 118 ++++++++ .../index.ts => cases/constants.ts} | 7 + .../connectors/cases/crypto_service.test.ts | 51 ++++ .../server/connectors/cases/crypto_service.ts | 26 ++ .../cases/server/connectors/cases/index.ts | 35 +++ .../cases/server/connectors/cases/schema.ts | 19 ++ .../cases/server/connectors/cases/types.ts | 49 +++ .../plugins/cases/server/connectors/index.ts | 7 + x-pack/plugins/cases/server/plugin.ts | 8 + .../server/saved_object_types/cases_oracle.ts | 72 +++++ .../cases/server/saved_object_types/index.ts | 1 + .../check_registered_connector_types.ts | 1 + .../check_registered_task_types.ts | 1 + 20 files changed, 752 insertions(+) create mode 100644 x-pack/plugins/cases/server/connectors/cases/cases_connector.ts create mode 100644 x-pack/plugins/cases/server/connectors/cases/cases_oracle_service.test.ts create mode 100644 x-pack/plugins/cases/server/connectors/cases/cases_oracle_service.ts rename x-pack/plugins/cases/server/connectors/{cases_action/index.ts => cases/constants.ts} (62%) create mode 100644 x-pack/plugins/cases/server/connectors/cases/crypto_service.test.ts create mode 100644 x-pack/plugins/cases/server/connectors/cases/crypto_service.ts create mode 100644 x-pack/plugins/cases/server/connectors/cases/index.ts create mode 100644 x-pack/plugins/cases/server/connectors/cases/schema.ts create mode 100644 x-pack/plugins/cases/server/connectors/cases/types.ts create mode 100644 x-pack/plugins/cases/server/saved_object_types/cases_oracle.ts diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 7fc18bb58a00b..cfc5289d92ddd 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -1465,6 +1465,34 @@ "dynamic": false, "properties": {} }, + "cases-oracle": { + "dynamic": false, + "properties": { + "cases": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "counter": { + "type": "unsigned_long" + }, + "createdAt": { + "type": "date" + }, + "rules": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "updatedAt": { + "type": "date" + } + } + }, "infrastructure-monitoring-log-view": { "dynamic": false, "properties": { diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts index 73fef09887c69..33b3530a0a3d8 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts @@ -74,6 +74,7 @@ describe('checking migration metadata changes on all registered SO types', () => "cases-comments": "5cb0a421588831c2a950e50f486048b8aabbae25", "cases-configure": "44ed7b8e0f44df39516b8870589b89e32224d2bf", "cases-connector-mappings": "f9d1ac57e484e69506c36a8051e4d61f4a8cfd25", + "cases-oracle": "afd99cd22b5551ac336b7c0f30f9ee31aa2b9f20", "cases-telemetry": "f219eb7e26772884342487fc9602cfea07b3cedc", "cases-user-actions": "483f10db9b3bd1617948d7032a98b7791bf87414", "config": "179b3e2bc672626aafce3cf92093a113f456af38", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts index 2cef3801868bd..cf8461f07713b 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts @@ -33,6 +33,7 @@ const previouslyRegisteredTypes = [ 'cases-comments', 'cases-configure', 'cases-connector-mappings', + 'cases-oracle', 'cases-sub-case', 'cases-user-actions', 'cases-telemetry', diff --git a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts index c39ceaf30da69..19856c39f8f86 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts @@ -195,6 +195,7 @@ describe('split .kibana index into multiple system indices', () => { "cases-comments", "cases-configure", "cases-connector-mappings", + "cases-oracle", "cases-telemetry", "cases-user-actions", "config", diff --git a/x-pack/plugins/cases/common/constants/index.ts b/x-pack/plugins/cases/common/constants/index.ts index f2c2dcef9bb2e..9a611a02eaeb2 100644 --- a/x-pack/plugins/cases/common/constants/index.ts +++ b/x-pack/plugins/cases/common/constants/index.ts @@ -24,6 +24,7 @@ export const CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT = 'cases-connector-mappings' a export const CASE_USER_ACTION_SAVED_OBJECT = 'cases-user-actions' as const; export const CASE_COMMENT_SAVED_OBJECT = 'cases-comments' as const; export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure' as const; +export const CASE_ORACLE_SAVED_OBJECT = 'cases-oracle' as const; /** * If more values are added here please also add them here: x-pack/test/cases_api_integration/common/plugins diff --git a/x-pack/plugins/cases/server/connectors/cases/cases_connector.ts b/x-pack/plugins/cases/server/connectors/cases/cases_connector.ts new file mode 100644 index 0000000000000..d7cee94276c51 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/cases/cases_connector.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ServiceParams } from '@kbn/actions-plugin/server'; +import { SubActionConnector } from '@kbn/actions-plugin/server'; +import { CASES_CONNECTOR_SUB_ACTION } from './constants'; +import type { CasesConnectorConfig, CasesConnectorSecrets } from './types'; +import { CasesConnectorParamsSchema } from './schema'; + +export class CasesConnector extends SubActionConnector< + CasesConnectorConfig, + CasesConnectorSecrets +> { + constructor(params: ServiceParams) { + super(params); + + this.registerSubActions(); + } + + private registerSubActions() { + this.registerSubAction({ + name: CASES_CONNECTOR_SUB_ACTION.RUN, + method: 'run', + schema: CasesConnectorParamsSchema, + }); + } + + /** + * Method is not needed for the Case Connector. + * The function throws an error as a reminder to + * implement it if we need it in the future. + */ + protected getResponseErrorMessage(): string { + throw new Error('Method not implemented.'); + } + + public async run() {} +} diff --git a/x-pack/plugins/cases/server/connectors/cases/cases_oracle_service.test.ts b/x-pack/plugins/cases/server/connectors/cases/cases_oracle_service.test.ts new file mode 100644 index 0000000000000..5d1261f9d9539 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/cases/cases_oracle_service.test.ts @@ -0,0 +1,283 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createHash } from 'node:crypto'; +import stringify from 'json-stable-stringify'; +import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; +import { loggerMock } from '@kbn/logging-mocks'; + +import { CasesOracleService } from './cases_oracle_service'; +import { CASE_ORACLE_SAVED_OBJECT } from '../../../common/constants'; +import { isEmpty, set } from 'lodash'; + +describe('CasesOracleService', () => { + const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + const mockLogger = loggerMock.create(); + + let service: CasesOracleService; + + beforeEach(() => { + jest.resetAllMocks(); + service = new CasesOracleService({ unsecuredSavedObjectsClient, log: mockLogger }); + }); + + describe('getRecordId', () => { + it('return the record ID correctly', async () => { + const ruleId = 'test-rule-id'; + const spaceId = 'default'; + const owner = 'cases'; + const grouping = { 'host.ip': '0.0.0.1' }; + + const payload = `${ruleId}:${spaceId}:${owner}:${stringify(grouping)}`; + const hash = createHash('sha256'); + + hash.update(payload); + + const hex = hash.digest('hex'); + + expect(service.getRecordId({ ruleId, spaceId, owner, grouping })).toEqual(hex); + }); + + it('sorts the grouping definition correctly', async () => { + const ruleId = 'test-rule-id'; + const spaceId = 'default'; + const owner = 'cases'; + const grouping = { 'host.ip': '0.0.0.1', 'agent.id': '8a4f500d' }; + const sortedGrouping = { 'agent.id': '8a4f500d', 'host.ip': '0.0.0.1' }; + + const payload = `${ruleId}:${spaceId}:${owner}:${stringify(sortedGrouping)}`; + const hash = createHash('sha256'); + + hash.update(payload); + + const hex = hash.digest('hex'); + + expect(service.getRecordId({ ruleId, spaceId, owner, grouping })).toEqual(hex); + }); + + it('return the record ID correctly without grouping', async () => { + const ruleId = 'test-rule-id'; + const spaceId = 'default'; + const owner = 'cases'; + + const payload = `${ruleId}:${spaceId}:${owner}`; + const hash = createHash('sha256'); + + hash.update(payload); + + const hex = hash.digest('hex'); + + expect(service.getRecordId({ ruleId, spaceId, owner })).toEqual(hex); + }); + + it('return the record ID correctly with empty grouping', async () => { + const ruleId = 'test-rule-id'; + const spaceId = 'default'; + const owner = 'cases'; + const grouping = {}; + + const payload = `${ruleId}:${spaceId}:${owner}:${stringify(grouping)}`; + const hash = createHash('sha256'); + + hash.update(payload); + + const hex = hash.digest('hex'); + + expect(service.getRecordId({ ruleId, spaceId, owner, grouping })).toEqual(hex); + }); + + it('return the record ID correctly without rule', async () => { + const spaceId = 'default'; + const owner = 'cases'; + const grouping = { 'host.ip': '0.0.0.1' }; + + const payload = `${spaceId}:${owner}:${stringify(grouping)}`; + const hash = createHash('sha256'); + + hash.update(payload); + + const hex = hash.digest('hex'); + + expect(service.getRecordId({ spaceId, owner, grouping })).toEqual(hex); + }); + + it('throws an error when the ruleId and the grouping is missing', async () => { + const spaceId = 'default'; + const owner = 'cases'; + + // @ts-expect-error: ruleId and grouping are omitted for testing + expect(() => service.getRecordId({ spaceId, owner })).toThrowErrorMatchingInlineSnapshot( + `"ruleID or grouping is required"` + ); + }); + + it.each(['ruleId', 'spaceId', 'owner'])( + 'return the record ID correctly with empty string for %s', + async (key) => { + const getPayloadValue = (value: string) => (isEmpty(value) ? '' : `${value}:`); + + const params = { + ruleId: 'test-rule-id', + spaceId: 'default', + owner: 'cases', + }; + + const grouping = { 'host.ip': '0.0.0.1' }; + + set(params, key, ''); + + const payload = `${getPayloadValue(params.ruleId)}${getPayloadValue( + params.spaceId + )}${getPayloadValue(params.owner)}${stringify(grouping)}`; + + const hash = createHash('sha256'); + + hash.update(payload); + + const hex = hash.digest('hex'); + + expect(service.getRecordId({ ...params, grouping })).toEqual(hex); + } + ); + }); + + describe('getRecord', () => { + const cases = [{ id: 'test-case-id' }]; + const rules = [{ id: 'test-rule-id' }]; + const grouping = { 'host.ip': '0.0.0.1', 'agent.id': '8a4f500d' }; + + const oracleSO = { + id: 'so-id', + version: 'so-version', + attributes: { + counter: 1, + cases, + rules, + grouping, + createdAt: '2023-10-10T10:23:42.769Z', + updatedAt: '2023-10-10T10:23:42.769Z', + }, + type: CASE_ORACLE_SAVED_OBJECT, + references: [], + }; + + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValue(oracleSO); + }); + + it('gets a record correctly', async () => { + const record = await service.getRecord('so-id'); + + expect(record).toEqual({ ...oracleSO.attributes, id: 'so-id', version: 'so-version' }); + }); + }); + + describe('createRecord', () => { + const cases = [{ id: 'test-case-id' }]; + const rules = [{ id: 'test-rule-id' }]; + const grouping = { 'host.ip': '0.0.0.1', 'agent.id': '8a4f500d' }; + + const oracleSO = { + id: 'so-id', + version: 'so-version', + attributes: { + counter: 1, + cases, + rules, + grouping, + createdAt: '2023-10-10T10:23:42.769Z', + updatedAt: '2023-10-10T10:23:42.769Z', + }, + type: CASE_ORACLE_SAVED_OBJECT, + references: [], + }; + + beforeEach(() => { + unsecuredSavedObjectsClient.create.mockResolvedValue(oracleSO); + }); + + it('creates a record correctly', async () => { + const record = await service.createRecord('so-id', { cases, rules, grouping }); + + expect(record).toEqual({ ...oracleSO.attributes, id: 'so-id', version: 'so-version' }); + }); + + it('calls the unsecuredSavedObjectsClient.create method correctly', async () => { + const id = 'so-id'; + + await service.createRecord(id, { cases, rules, grouping }); + + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( + 'cases-oracle', + { + cases, + counter: 1, + createdAt: expect.anything(), + rules, + grouping, + updatedAt: null, + }, + { id } + ); + }); + }); + + describe('increaseCounter', () => { + const cases = [{ id: 'test-case-id' }]; + const rules = [{ id: 'test-rule-id' }]; + const grouping = { 'host.ip': '0.0.0.1', 'agent.id': '8a4f500d' }; + + const oracleSO = { + id: 'so-id', + version: 'so-version', + attributes: { + counter: 1, + cases, + rules, + grouping, + createdAt: '2023-10-10T10:23:42.769Z', + updatedAt: '2023-10-10T10:23:42.769Z', + }, + type: CASE_ORACLE_SAVED_OBJECT, + references: [], + }; + + const oracleSOWithIncreasedCounter = { + ...oracleSO, + attributes: { ...oracleSO.attributes, counter: 2 }, + }; + + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValue(oracleSO); + unsecuredSavedObjectsClient.update.mockResolvedValue(oracleSOWithIncreasedCounter); + }); + + it('increases the counter correctly', async () => { + const record = await service.increaseCounter('so-id'); + + expect(record).toEqual({ + ...oracleSO.attributes, + id: 'so-id', + version: 'so-version', + counter: 2, + }); + }); + + it('calls the unsecuredSavedObjectsClient.update method correctly', async () => { + await service.increaseCounter('so-id'); + + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'cases-oracle', + 'so-id', + { + counter: 2, + }, + { version: 'so-version' } + ); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/connectors/cases/cases_oracle_service.ts b/x-pack/plugins/cases/server/connectors/cases/cases_oracle_service.ts new file mode 100644 index 0000000000000..816df7719f036 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/cases/cases_oracle_service.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger, SavedObject, SavedObjectsClientContract } from '@kbn/core/server'; +import { CASE_ORACLE_SAVED_OBJECT } from '../../../common/constants'; +import { CryptoService } from './crypto_service'; +import type { OracleKey, OracleRecord, OracleRecordCreateRequest } from './types'; + +type OracleRecordAttributes = Omit; + +export class CasesOracleService { + private readonly log: Logger; + private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; + private cryptoService: CryptoService; + + constructor({ + log, + unsecuredSavedObjectsClient, + }: { + log: Logger; + unsecuredSavedObjectsClient: SavedObjectsClientContract; + }) { + this.log = log; + this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; + this.cryptoService = new CryptoService(); + } + + public getRecordId({ ruleId, spaceId, owner, grouping }: OracleKey): string { + if (grouping == null && ruleId == null) { + throw new Error('ruleID or grouping is required'); + } + + const payload = [ + ruleId, + spaceId, + owner, + this.cryptoService.stringifyDeterministically(grouping), + ] + .filter(Boolean) + .join(':'); + + return this.cryptoService.getHash(payload); + } + + public async getRecord(recordId: string): Promise { + this.log.debug(`Getting oracle record with ID: ${recordId}`); + + const oracleRecord = await this.unsecuredSavedObjectsClient.get( + CASE_ORACLE_SAVED_OBJECT, + recordId + ); + + return this.getRecordResponse(oracleRecord); + } + + public async createRecord( + recordId: string, + payload: OracleRecordCreateRequest + ): Promise { + const { cases, rules, grouping } = payload; + + this.log.debug(`Creating oracle record with ID: ${recordId}`); + + const oracleRecord = await this.unsecuredSavedObjectsClient.create( + CASE_ORACLE_SAVED_OBJECT, + { + counter: 1, + cases, + rules, + grouping, + createdAt: new Date().toISOString(), + updatedAt: null, + }, + { id: recordId } + ); + + return this.getRecordResponse(oracleRecord); + } + + public async increaseCounter(recordId: string): Promise { + const { id: _, version, ...record } = await this.getRecord(recordId); + const newCounter = record.counter + 1; + + this.log.debug( + `Increasing the counter of oracle record with ID: ${recordId} from ${record.counter} to ${newCounter}` + ); + + const oracleRecord = await this.unsecuredSavedObjectsClient.update( + CASE_ORACLE_SAVED_OBJECT, + recordId, + { counter: newCounter }, + { version } + ); + + return this.getRecordResponse({ + ...oracleRecord, + attributes: { ...record, counter: newCounter }, + references: oracleRecord.references ?? [], + }); + } + + private getRecordResponse = ( + oracleRecord: SavedObject + ): OracleRecord => ({ + id: oracleRecord.id, + version: oracleRecord.version ?? '', + counter: oracleRecord.attributes.counter, + cases: oracleRecord.attributes.cases, + grouping: oracleRecord.attributes.grouping, + rules: oracleRecord.attributes.rules, + createdAt: oracleRecord.attributes.createdAt, + updatedAt: oracleRecord.attributes.updatedAt, + }); +} diff --git a/x-pack/plugins/cases/server/connectors/cases_action/index.ts b/x-pack/plugins/cases/server/connectors/cases/constants.ts similarity index 62% rename from x-pack/plugins/cases/server/connectors/cases_action/index.ts rename to x-pack/plugins/cases/server/connectors/cases/constants.ts index 1fec1c76430eb..bea96fcb4f387 100644 --- a/x-pack/plugins/cases/server/connectors/cases_action/index.ts +++ b/x-pack/plugins/cases/server/connectors/cases/constants.ts @@ -4,3 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +export const CASES_CONNECTOR_ID = '.cases'; +export const CASES_CONNECTOR_TITLE = 'Cases'; + +export enum CASES_CONNECTOR_SUB_ACTION { + RUN = 'run', +} diff --git a/x-pack/plugins/cases/server/connectors/cases/crypto_service.test.ts b/x-pack/plugins/cases/server/connectors/cases/crypto_service.test.ts new file mode 100644 index 0000000000000..bf8a9f946ab58 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/cases/crypto_service.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createHash } from 'node:crypto'; +import { CryptoService } from './crypto_service'; + +describe('CryptoService', () => { + let service: CryptoService; + + beforeEach(() => { + jest.resetAllMocks(); + service = new CryptoService(); + }); + + describe('getHash', () => { + it('returns the sha256 of a payload correctly', async () => { + const payload = 'my payload'; + const hash = createHash('sha256'); + + hash.update(payload); + + const hex = hash.digest('hex'); + + expect(service.getHash(payload)).toEqual(hex); + }); + + it('creates a new instance of the hash function on each call', async () => { + const payload = 'my payload'; + const hash = createHash('sha256'); + + hash.update(payload); + + const hex = hash.digest('hex'); + + expect(service.getHash(payload)).toEqual(hex); + expect(service.getHash(payload)).toEqual(hex); + }); + }); + + describe('stringifyDeterministically', () => { + it('deterministically stringifies an object', async () => { + expect( + service.stringifyDeterministically({ 'host.ip': '0.0.0.1', 'agent.id': '8a4f500d' }) + ).toEqual('{"agent.id":"8a4f500d","host.ip":"0.0.0.1"}'); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/connectors/cases/crypto_service.ts b/x-pack/plugins/cases/server/connectors/cases/crypto_service.ts new file mode 100644 index 0000000000000..e35b4e51ed1b4 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/cases/crypto_service.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createHash } from 'node:crypto'; +import stringify from 'json-stable-stringify'; + +export class CryptoService { + public getHash(payload: string): string { + const hash = createHash('sha256'); + + hash.update(payload); + return hash.digest('hex'); + } + + public stringifyDeterministically(obj?: Record): string | null { + if (obj == null) { + return null; + } + + return stringify(obj); + } +} diff --git a/x-pack/plugins/cases/server/connectors/cases/index.ts b/x-pack/plugins/cases/server/connectors/cases/index.ts new file mode 100644 index 0000000000000..244474f286b8f --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/cases/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SecurityConnectorFeatureId, UptimeConnectorFeatureId } from '@kbn/actions-plugin/common'; +import type { SubActionConnectorType } from '@kbn/actions-plugin/server/sub_action_framework/types'; +import { CasesConnector } from './cases_connector'; +import { CASES_CONNECTOR_ID, CASES_CONNECTOR_TITLE } from './constants'; +import type { CasesConnectorConfig, CasesConnectorSecrets } from './types'; +import { CasesConnectorConfigSchema, CasesConnectorSecretsSchema } from './schema'; + +export const getCasesConnectorType = (): SubActionConnectorType< + CasesConnectorConfig, + CasesConnectorSecrets +> => ({ + id: CASES_CONNECTOR_ID, + name: CASES_CONNECTOR_TITLE, + Service: CasesConnector, + schema: { + config: CasesConnectorConfigSchema, + secrets: CasesConnectorSecretsSchema, + }, + /** + * TODO: Limit only to rule types that support + * alerts-as-data + */ + supportedFeatureIds: [SecurityConnectorFeatureId, UptimeConnectorFeatureId], + /** + * TODO: Verify license + */ + minimumLicenseRequired: 'platinum' as const, +}); diff --git a/x-pack/plugins/cases/server/connectors/cases/schema.ts b/x-pack/plugins/cases/server/connectors/cases/schema.ts new file mode 100644 index 0000000000000..4b6aedaee02d7 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/cases/schema.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +/** + * The case connector does not have any configuration + * or secrets. + */ +export const CasesConnectorConfigSchema = schema.object({}); +export const CasesConnectorSecretsSchema = schema.object({}); +/** + * TODO: Add needed properties in the params schema. + */ +export const CasesConnectorParamsSchema = schema.object({}); diff --git a/x-pack/plugins/cases/server/connectors/cases/types.ts b/x-pack/plugins/cases/server/connectors/cases/types.ts new file mode 100644 index 0000000000000..373c24ed7e690 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/cases/types.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ExclusiveUnion } from '@elastic/eui'; +import type { TypeOf } from '@kbn/config-schema'; +import type { + CasesConnectorConfigSchema, + CasesConnectorSecretsSchema, + CasesConnectorParamsSchema, +} from './schema'; + +export type CasesConnectorConfig = TypeOf; +export type CasesConnectorSecrets = TypeOf; +export type CasesConnectorParams = TypeOf; + +type Optional = Pick, K> & Omit; + +interface OracleKeyAllRequired { + ruleId: string; + spaceId: string; + owner: string; + grouping: Record; +} + +type OracleKeyWithOptionalKey = Optional; +type OracleKeyWithOptionalGrouping = Optional; + +export type OracleKey = ExclusiveUnion; + +export interface OracleRecord { + id: string; + counter: number; + cases: Array<{ id: string }>; + grouping: Record; + rules: Array<{ id: string }>; + createdAt: string; + updatedAt: string | null; + version: string; +} + +export interface OracleRecordCreateRequest { + cases: Array<{ id: string }>; + rules: Array<{ id: string }>; + grouping: Record; +} diff --git a/x-pack/plugins/cases/server/connectors/index.ts b/x-pack/plugins/cases/server/connectors/index.ts index 78b83223a3d66..329eb6a911133 100644 --- a/x-pack/plugins/cases/server/connectors/index.ts +++ b/x-pack/plugins/cases/server/connectors/index.ts @@ -5,5 +5,12 @@ * 2.0. */ +import type { PluginSetupContract as ActionsPluginSetupContract } from '@kbn/actions-plugin/server'; +import { getCasesConnectorType } from './cases'; + export * from './types'; export { casesConnectors } from './factory'; + +export function registerConnectorTypes({ actions }: { actions: ActionsPluginSetupContract }) { + actions.registerSubActionConnectorType(getCasesConnectorType()); +} diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 510686f1a98bd..66460404556b0 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -43,6 +43,7 @@ import { createCaseSavedObjectType, createCaseUserActionSavedObjectType, casesTelemetrySavedObjectType, + casesOracleSavedObjectType, } from './saved_object_types'; import type { CasesClient } from './client'; @@ -60,6 +61,7 @@ import { LICENSING_CASE_ASSIGNMENT_FEATURE } from './common/constants'; import { registerInternalAttachments } from './internal_attachments'; import { registerCaseFileKinds } from './files'; import type { ConfigType } from './config'; +import { registerConnectorTypes } from './connectors'; export interface PluginsSetup { actions: ActionsPluginSetup; @@ -116,6 +118,7 @@ export class CasePlugin { this.externalReferenceAttachmentTypeRegistry, this.persistableStateAttachmentTypeRegistry ); + registerCaseFileKinds(this.caseConfig.files, plugins.files); this.securityPluginSetup = plugins.security; @@ -133,6 +136,7 @@ export class CasePlugin { }, }) ); + core.savedObjects.registerType(caseConfigureSavedObjectType); core.savedObjects.registerType(caseConnectorMappingsSavedObjectType); core.savedObjects.registerType(createCaseSavedObjectType(core, this.logger)); @@ -141,7 +145,9 @@ export class CasePlugin { persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry, }) ); + core.savedObjects.registerType(casesTelemetrySavedObjectType); + core.savedObjects.registerType(casesOracleSavedObjectType); core.http.registerRouteHandlerContext( APP_ID, @@ -173,6 +179,8 @@ export class CasePlugin { plugins.licensing.featureUsage.register(LICENSING_CASE_ASSIGNMENT_FEATURE, 'platinum'); + registerConnectorTypes({ actions: plugins.actions }); + return { attachmentFramework: { registerExternalReference: (externalReferenceAttachmentType) => { diff --git a/x-pack/plugins/cases/server/saved_object_types/cases_oracle.ts b/x-pack/plugins/cases/server/saved_object_types/cases_oracle.ts new file mode 100644 index 0000000000000..3419b607d0276 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/cases_oracle.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsType } from '@kbn/core/server'; +import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; +import { schema } from '@kbn/config-schema'; +import { CASE_ORACLE_SAVED_OBJECT } from '../../common/constants'; + +export const casesOracleSavedObjectType: SavedObjectsType = { + name: CASE_ORACLE_SAVED_OBJECT, + indexPattern: ALERTING_CASES_SAVED_OBJECT_INDEX, + hidden: true, + /** + * TODO: Verify + */ + namespaceType: 'agnostic', + mappings: { + dynamic: false, + properties: { + cases: { + properties: { + id: { + type: 'keyword', + }, + }, + }, + counter: { + type: 'unsigned_long', + }, + createdAt: { + type: 'date', + }, + /* + grouping: { + type: 'flattened', + }, + */ + rules: { + properties: { + id: { + type: 'keyword', + }, + }, + }, + updatedAt: { + type: 'date', + }, + }, + }, + management: { + importableAndExportable: false, + }, + modelVersions: { + '1': { + changes: [], + schemas: { + create: schema.object({ + cases: schema.arrayOf(schema.object({ id: schema.string() })), + counter: schema.number(), + createdAt: schema.string(), + grouping: schema.recordOf(schema.string(), schema.any()), + rules: schema.arrayOf(schema.object({ id: schema.string() })), + updatedAt: schema.string(), + }), + }, + }, + }, +}; diff --git a/x-pack/plugins/cases/server/saved_object_types/index.ts b/x-pack/plugins/cases/server/saved_object_types/index.ts index a43e60c0a240b..47fb1f252d783 100644 --- a/x-pack/plugins/cases/server/saved_object_types/index.ts +++ b/x-pack/plugins/cases/server/saved_object_types/index.ts @@ -11,3 +11,4 @@ export { createCaseCommentSavedObjectType } from './comments'; export { createCaseUserActionSavedObjectType } from './user_actions'; export { caseConnectorMappingsSavedObjectType } from './connector_mappings'; export { casesTelemetrySavedObjectType } from './telemetry'; +export { casesOracleSavedObjectType } from './cases_oracle'; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/check_registered_connector_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/check_registered_connector_types.ts index 13fcc069a4df3..243e12a92e938 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/check_registered_connector_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/check_registered_connector_types.ts @@ -50,6 +50,7 @@ export default function createRegisteredConnectorTypeTests({ getService }: FtrPr '.opsgenie', '.gen-ai', '.bedrock', + '.cases', ].sort() ); }); diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts index 7e48895a9060f..e13864d6fd165 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts @@ -52,6 +52,7 @@ export default function ({ getService }: FtrProviderContext) { 'Synthetics:Clean-Up-Package-Policies', 'UPTIME:SyntheticsService:Sync-Saved-Monitor-Objects', 'actions:.bedrock', + 'actions:.cases', 'actions:.cases-webhook', 'actions:.d3security', 'actions:.email',