diff --git a/x-pack/plugins/actions/server/create_system_actions.test.ts b/x-pack/plugins/actions/server/create_system_actions.test.ts index 55b7d43bf5631..fde7bed65ab61 100644 --- a/x-pack/plugins/actions/server/create_system_actions.test.ts +++ b/x-pack/plugins/actions/server/create_system_actions.test.ts @@ -36,7 +36,7 @@ describe('createSystemConnectors', () => { { id: 'system-connector-system-action-type-2', actionTypeId: 'system-action-type-2', - name: 'System action: system-action-type-2', + name: 'My system action type', secrets: {}, config: {}, isDeprecated: false, diff --git a/x-pack/plugins/actions/server/create_system_actions.ts b/x-pack/plugins/actions/server/create_system_actions.ts index f6079ea940222..604b5589faa47 100644 --- a/x-pack/plugins/actions/server/create_system_actions.ts +++ b/x-pack/plugins/actions/server/create_system_actions.ts @@ -14,7 +14,7 @@ export const createSystemConnectors = (actionTypes: ActionType[]): InMemoryConne const systemConnectors: InMemoryConnector[] = systemActionTypes.map((systemActionType) => ({ id: `system-connector-${systemActionType.id}`, actionTypeId: systemActionType.id, - name: `System action: ${systemActionType.id}`, + name: systemActionType.name, isMissingSecrets: false, config: {}, secrets: {}, diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 4d589699a2caa..9737afdb095c0 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -263,7 +263,7 @@ describe('Actions Plugin', () => { { id: 'system-connector-.cases', actionTypeId: '.cases', - name: 'System action: .cases', + name: 'Cases', config: {}, secrets: {}, isDeprecated: false, @@ -769,7 +769,7 @@ describe('Actions Plugin', () => { { id: 'system-connector-.cases', actionTypeId: '.cases', - name: 'System action: .cases', + name: 'Cases', config: {}, secrets: {}, isDeprecated: false, diff --git a/x-pack/plugins/alerting/server/connector_adapters/types.ts b/x-pack/plugins/alerting/server/connector_adapters/types.ts index e2bd2d7ed2c93..744b647878c06 100644 --- a/x-pack/plugins/alerting/server/connector_adapters/types.ts +++ b/x-pack/plugins/alerting/server/connector_adapters/types.ts @@ -9,7 +9,7 @@ import { ObjectType } from '@kbn/config-schema'; import type { RuleTypeParams, SanitizedRule } from '../../common'; import { CombinedSummarizedAlerts } from '../types'; -type Rule = Pick, 'id' | 'name' | 'tags'>; +type Rule = Pick, 'id' | 'name' | 'tags' | 'consumer'>; export interface ConnectorAdapterParams { [x: string]: unknown; diff --git a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts index 5f5a97c842e33..47022933e93a1 100644 --- a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts @@ -115,6 +115,7 @@ const rule = { uuid: '111-111', }, ], + consumer: 'test-consumer', } as unknown as SanitizedRule; const defaultExecutionParams = { @@ -2472,6 +2473,7 @@ describe('Execution Handler', () => { id: rule.id, name: rule.name, tags: rule.tags, + consumer: 'test-consumer', }, ruleUrl: 'https://example.com/s/test1/app/management/insightsAndAlerting/triggersActions/rule/1', diff --git a/x-pack/plugins/alerting/server/task_runner/execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/execution_handler.ts index f794133c69dc7..e268162b88f1b 100644 --- a/x-pack/plugins/alerting/server/task_runner/execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/execution_handler.ts @@ -459,7 +459,7 @@ export class ExecutionHandler< const connectorAdapterActionParams = connectorAdapter.buildActionParams({ alerts: summarizedAlerts, - rule: { id: rule.id, tags: rule.tags, name: rule.name }, + rule: { id: rule.id, tags: rule.tags, name: rule.name, consumer: rule.consumer }, ruleUrl: ruleUrl?.absoluteUrl, spaceId, params: action.params, diff --git a/x-pack/plugins/cases/common/constants/owner.test.ts b/x-pack/plugins/cases/common/constants/owner.test.ts new file mode 100644 index 0000000000000..07b6866f857a4 --- /dev/null +++ b/x-pack/plugins/cases/common/constants/owner.test.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 { AlertConsumers } from '@kbn/rule-data-utils'; +import { OWNER_INFO } from './owners'; + +describe('OWNER_INFO', () => { + it('should use all available rule consumers', () => { + const allConsumers = new Set(Object.values(AlertConsumers)); + const ownersMappingConsumers = new Set( + Object.values(OWNER_INFO) + .map((value) => value.validRuleConsumers ?? []) + .flat() + ); + + expect(allConsumers.size).toEqual(ownersMappingConsumers.size); + + for (const consumer of allConsumers) { + expect(ownersMappingConsumers.has(consumer)).toBe(true); + } + }); +}); diff --git a/x-pack/plugins/cases/common/constants/owners.ts b/x-pack/plugins/cases/common/constants/owners.ts index 3e799030c7d5b..8ac7164ef75cc 100644 --- a/x-pack/plugins/cases/common/constants/owners.ts +++ b/x-pack/plugins/cases/common/constants/owners.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { AlertConsumers } from '@kbn/rule-data-utils'; import { APP_ID } from './application'; import type { Owner } from './types'; @@ -23,6 +24,7 @@ interface RouteInfo { label: string; iconType: string; appRoute: string; + validRuleConsumers?: readonly AlertConsumers[]; } export const OWNER_INFO: Record = { @@ -32,6 +34,7 @@ export const OWNER_INFO: Record = { label: 'Security', iconType: 'logoSecurity', appRoute: '/app/security', + validRuleConsumers: [AlertConsumers.SIEM], }, [OBSERVABILITY_OWNER]: { id: OBSERVABILITY_OWNER, @@ -39,6 +42,16 @@ export const OWNER_INFO: Record = { label: 'Observability', iconType: 'logoObservability', appRoute: '/app/observability', + validRuleConsumers: [ + // only valid in serverless + AlertConsumers.OBSERVABILITY, + AlertConsumers.APM, + AlertConsumers.INFRASTRUCTURE, + AlertConsumers.LOGS, + AlertConsumers.SLO, + AlertConsumers.UPTIME, + AlertConsumers.MONITORING, + ], }, [GENERAL_CASES_OWNER]: { id: GENERAL_CASES_OWNER, @@ -46,5 +59,6 @@ export const OWNER_INFO: Record = { label: 'Stack', iconType: 'casesApp', appRoute: '/app/management/insightsAndAlerting', + validRuleConsumers: [AlertConsumers.ML, AlertConsumers.STACK_ALERTS, AlertConsumers.EXAMPLE], }, } as const; diff --git a/x-pack/plugins/cases/kibana.jsonc b/x-pack/plugins/cases/kibana.jsonc index cd4fc787af2e0..feecbc66ee445 100644 --- a/x-pack/plugins/cases/kibana.jsonc +++ b/x-pack/plugins/cases/kibana.jsonc @@ -12,6 +12,7 @@ "cases" ], "requiredPlugins": [ + "alerting", "actions", "data", "embeddable", diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/cases.tsx b/x-pack/plugins/cases/public/components/system_actions/cases/cases.tsx index ddbfbdb5d6bd2..0506b2069a3c4 100644 --- a/x-pack/plugins/cases/public/components/system_actions/cases/cases.tsx +++ b/x-pack/plugins/cases/public/components/system_actions/cases/cases.tsx @@ -53,5 +53,6 @@ export function getConnectorType(): ConnectorTypeModel<{}, {}, CasesActionParams return validationResult; }, actionParamsFields: lazy(() => import('./cases_params')), + isSystemActionType: true, }; } diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.test.tsx b/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.test.tsx index 89b50bb2ef5a3..a59ab255ba429 100644 --- a/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.test.tsx +++ b/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.test.tsx @@ -24,7 +24,6 @@ const useApplicationMock = useApplication as jest.Mock; const actionParams = { subAction: 'run', subActionParams: { - owner: 'cases', timeWindow: '6w', reopenClosedCases: false, groupingBy: [], @@ -120,24 +119,9 @@ describe('CasesParamsFields renders', () => { timeWindow: '7d', reopenClosedCases: false, groupingBy: [], - owner: 'cases', }); }); - it('sets owner to default if appId not matched', async () => { - useApplicationMock.mockReturnValue({ appId: 'testAppId' }); - - const newProps = { - ...defaultProps, - actionParams: { - subAction: 'run', - }, - }; - render(); - - expect(editAction.mock.calls[0][1].owner).toEqual('cases'); - }); - it('If timeWindow has errors, form row is invalid', async () => { const newProps = { ...defaultProps, @@ -149,25 +133,6 @@ describe('CasesParamsFields renders', () => { expect(await screen.findByText('error')).toBeInTheDocument(); }); - it('updates owner correctly', async () => { - useApplicationMock.mockReturnValueOnce({ appId: 'securitySolutionUI' }); - - const newProps = { - ...defaultProps, - actionParams: { - subAction: 'run', - }, - }; - - const { rerender } = render(); - - expect(editAction.mock.calls[0][1].owner).toEqual('cases'); - - rerender(); - - expect(editAction.mock.calls[1][1].owner).toEqual('securitySolution'); - }); - describe('UI updates', () => { it('renders grouping by field options', async () => { render(); 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 fdfbca7de63dd..5c18555cd0855 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 @@ -20,9 +20,7 @@ import { EuiComboBox, } from '@elastic/eui'; import type { ValidFeatureId } from '@kbn/rule-data-utils'; -import { useApplication } from '../../../common/lib/kibana/use_application'; -import { getCaseOwnerByAppId } from '../../../../common/utils/owner'; -import { CASES_CONNECTOR_SUB_ACTION, OWNER_INFO } from '../../../../common/constants'; +import { CASES_CONNECTOR_SUB_ACTION } from '../../../../common/constants'; import * as i18n from './translations'; import type { CasesActionParams } from './types'; import { DEFAULT_TIME_WINDOW, TIME_UNITS } from './constants'; @@ -32,9 +30,6 @@ import { useAlertDataViews } from '../hooks/use_alert_data_view'; export const CasesParamsFieldsComponent: React.FunctionComponent< ActionParamsProps > = ({ actionParams, editAction, errors, index, producerId }) => { - const { appId } = useApplication(); - const owner = getCaseOwnerByAppId(appId); - const { dataViews, loading: loadingAlertDataViews } = useAlertDataViews( producerId ? [producerId as ValidFeatureId] : [] ); @@ -70,25 +65,13 @@ export const CasesParamsFieldsComponent: React.FunctionComponent< timeWindow: `${DEFAULT_TIME_WINDOW}`, reopenClosedCases: false, groupingBy: [], - owner: OWNER_INFO.cases.id, - }, - index - ); - } - - if (actionParams.subActionParams && actionParams.subActionParams?.owner !== owner) { - editAction( - 'subActionParams', - { - ...actionParams.subActionParams, - owner, }, index ); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [actionParams, owner, appId]); + }, [actionParams]); const editSubActionProperty = useCallback( (key: string, value: unknown) => { diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/types.ts b/x-pack/plugins/cases/public/components/system_actions/cases/types.ts index da83a8a10f439..cea6110817f40 100644 --- a/x-pack/plugins/cases/public/components/system_actions/cases/types.ts +++ b/x-pack/plugins/cases/public/components/system_actions/cases/types.ts @@ -9,7 +9,6 @@ export interface CasesSubActionParamsUI { timeWindow: string; reopenClosedCases: boolean; groupingBy: string[]; - owner: string; } export interface CasesActionParams { subAction: string; diff --git a/x-pack/plugins/cases/server/connectors/cases/cases_connector_executor.test.ts b/x-pack/plugins/cases/server/connectors/cases/cases_connector_executor.test.ts index e3d32eb0eb061..dd3e75c48f4cd 100644 --- a/x-pack/plugins/cases/server/connectors/cases/cases_connector_executor.test.ts +++ b/x-pack/plugins/cases/server/connectors/cases/cases_connector_executor.test.ts @@ -31,6 +31,7 @@ import { timeWindow, reopenClosedCases, updatedCounterOracleRecord, + alertsNested, } from './index.mock'; import { expectCasesToHaveTheCorrectAlertsAttachedWithGrouping, @@ -631,7 +632,7 @@ describe('CasesConnectorExecutor', () => { casesClientMock.cases.bulkCreate.mock.calls[0][0].cases[0].description; expect(description).toBe( - 'This case is auto-created by [Test rule](https://example.com/rules/rule-test-id). \n\n Grouping: `foo` equals `["bar",1,true,{}]` and `bar` equals `{"foo":"test"}` and `baz` equals `my value`' + 'This case is auto-created by [Test rule](https://example.com/rules/rule-test-id). \n\n Grouping: `foo` equals `["bar",1,true,{}]` and `bar.foo` equals `test` and `baz` equals `my value`' ); }); @@ -759,7 +760,7 @@ describe('CasesConnectorExecutor', () => { 'auto-generated', 'rule:rule-test-id', 'foo:["bar",1,true,{}]', - 'bar:{"foo":"test"}', + 'bar.foo:test', 'baz:my value', 'rule', 'test', @@ -1010,6 +1011,12 @@ describe('CasesConnectorExecutor', () => { expectCasesToHaveTheCorrectAlertsAttachedWithGrouping(casesClientMock); }); + it('attach alerts with nested grouping', async () => { + await connectorExecutor.execute({ ...params, alerts: alertsNested }); + + expectCasesToHaveTheCorrectAlertsAttachedWithGrouping(casesClientMock); + }); + it('attaches alerts to reopened cases', async () => { casesClientMock.cases.bulkGet.mockResolvedValue({ cases: [{ ...cases[0], status: CaseStatuses.closed }], @@ -1378,6 +1385,25 @@ describe('CasesConnectorExecutor', () => { ).rejects.toThrowErrorMatchingInlineSnapshot(`"get configuration error"`); }); }); + + describe('Skipping execution', () => { + it('skips execution if alerts cannot be grouped', async () => { + await connectorExecutor.execute({ + ...params, + groupingBy: ['does.not.exists'], + }); + + expect(mockGetRecordId).not.toHaveBeenCalled(); + expect(mockBulkGetRecords).not.toHaveBeenCalled(); + expect(mockBulkCreateRecords).not.toHaveBeenCalled(); + expect(mockBulkUpdateRecord).not.toHaveBeenCalled(); + expect(mockGetCaseId).not.toHaveBeenCalled(); + expect(casesClientMock.cases.bulkGet).not.toHaveBeenCalled(); + expect(casesClientMock.cases.bulkCreate).not.toHaveBeenCalled(); + expect(casesClientMock.cases.bulkUpdate).not.toHaveBeenCalled(); + expect(casesClientMock.configure.get).not.toHaveBeenCalled(); + }); + }); }); }); diff --git a/x-pack/plugins/cases/server/connectors/cases/cases_connector_executor.ts b/x-pack/plugins/cases/server/connectors/cases/cases_connector_executor.ts index 3ad5472de5c6f..d961cec5c26bf 100644 --- a/x-pack/plugins/cases/server/connectors/cases/cases_connector_executor.ts +++ b/x-pack/plugins/cases/server/connectors/cases/cases_connector_executor.ts @@ -7,11 +7,12 @@ import stringify from 'json-stable-stringify'; import pMap from 'p-map'; -import { partition, pick } from 'lodash'; +import { get, partition, pick } from 'lodash'; import dateMath from '@kbn/datemath'; import { CaseStatuses } from '@kbn/cases-components'; import type { SavedObjectError } from '@kbn/core-saved-objects-common'; import type { Logger } from '@kbn/core/server'; +import { getFlattenedObject } from '@kbn/std'; import type { CustomFieldsConfiguration } from '../../../common/types/domain'; import { MAX_ALERTS_PER_CASE, @@ -84,6 +85,15 @@ export class CasesConnectorExecutor { const groupedAlerts = this.groupAlerts({ params, alerts, groupingBy }); const groupedAlertsWithCircuitBreakers = this.applyCircuitBreakers(params, groupedAlerts); + if (groupedAlertsWithCircuitBreakers.length === 0) { + this.logger.debug( + `[CasesConnector][CasesConnectorExecutor] Grouping did not produce any alerts. Skipping execution.`, + this.getLogMetadata(params) + ); + + return; + } + /** * Based on the rule ID, the grouping, the owner, the space ID, * the oracle record ID is generated @@ -170,7 +180,7 @@ export class CasesConnectorExecutor { * alerts will not be attached to any case. */ const filteredAlerts = alerts.filter((alert) => - uniqueGroupingByFields.every((groupingByField) => Object.hasOwn(alert, groupingByField)) + uniqueGroupingByFields.every((groupingByField) => Boolean(get(alert, groupingByField, null))) ); this.logger.debug( @@ -728,7 +738,9 @@ export class CasesConnectorExecutor { } private getGroupingDescription(grouping: GroupedAlerts['grouping']) { - return Object.entries(grouping) + const flattenGrouping = getFlattenedObject(grouping); + + return Object.entries(flattenGrouping) .map(([key, value]) => { const keyAsCodeBlock = `\`${key}\``; const valueAsCodeBlock = `\`${convertValueToString(value)}\``; @@ -752,7 +764,10 @@ export class CasesConnectorExecutor { } private getGroupingAsTags(grouping: GroupedAlerts['grouping']) { - return Object.entries(grouping).map(([key, value]) => `${key}:${convertValueToString(value)}`); + const flattenGrouping = getFlattenedObject(grouping); + return Object.entries(flattenGrouping).map( + ([key, value]) => `${key}:${convertValueToString(value)}` + ); } private async handleClosedCases( 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 index cf9930410208d..457b55662b272 100644 --- 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 @@ -304,7 +304,21 @@ describe('CasesOracleService', () => { grouping, updatedAt: null, }, - { id } + { + id, + references: [ + { + id: 'test-rule-id', + name: 'associated-alert', + type: 'alert', + }, + { + id: 'test-case-id', + name: 'associated-cases', + type: 'cases', + }, + ], + } ); }); }); @@ -375,6 +389,18 @@ describe('CasesOracleService', () => { }, id: 'so-id', type: 'cases-oracle', + references: [ + { + id: 'test-rule-id', + name: 'associated-alert', + type: 'alert', + }, + { + id: 'test-case-id', + name: 'associated-cases', + type: 'cases', + }, + ], }, { attributes: { @@ -387,6 +413,18 @@ describe('CasesOracleService', () => { }, id: 'so-id-2', type: 'cases-oracle', + references: [ + { + id: 'test-rule-id', + name: 'associated-alert', + type: 'alert', + }, + { + id: 'test-case-id', + name: 'associated-cases', + type: 'cases', + }, + ], }, ]); }); 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 index 24e8dce1c3a9a..ef5709dcbd957 100644 --- a/x-pack/plugins/cases/server/connectors/cases/cases_oracle_service.ts +++ b/x-pack/plugins/cases/server/connectors/cases/cases_oracle_service.ts @@ -5,8 +5,14 @@ * 2.0. */ -import type { Logger, SavedObject, SavedObjectsClientContract } from '@kbn/core/server'; -import { CASE_ORACLE_SAVED_OBJECT } from '../../../common/constants'; +import { RULE_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/server'; +import type { + Logger, + SavedObject, + SavedObjectReference, + SavedObjectsClientContract, +} from '@kbn/core/server'; +import { CASE_ORACLE_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../../common/constants'; import { isSODecoratedError, isSOError } from '../../common/error'; import type { SavedObjectsBulkResponseWithErrors } from '../../common/types'; import { INITIAL_ORACLE_RECORD_COUNTER } from './constants'; @@ -97,7 +103,7 @@ export class CasesOracleService { const oracleRecord = await this.savedObjectsClient.create( CASE_ORACLE_SAVED_OBJECT, this.getCreateRecordAttributes(payload), - { id: recordId } + { id: recordId, references: this.getCreateRecordReferences(payload) } ); return this.getRecordResponse(oracleRecord); @@ -120,6 +126,7 @@ export class CasesOracleService { id: record.recordId, type: CASE_ORACLE_SAVED_OBJECT, attributes: this.getCreateRecordAttributes(record.payload), + references: this.getCreateRecordReferences(record.payload), })); const oracleRecords = (await this.savedObjectsClient.bulkCreate( @@ -234,4 +241,30 @@ export class CasesOracleService { updatedAt: null, }; } + + private getCreateRecordReferences({ + cases, + rules, + grouping, + }: OracleRecordCreateRequest): SavedObjectReference[] { + const references = []; + + for (const rule of rules) { + references.push({ + id: rule.id, + type: RULE_SAVED_OBJECT_TYPE, + name: `associated-${RULE_SAVED_OBJECT_TYPE}`, + }); + } + + for (const theCase of cases) { + references.push({ + id: theCase.id, + type: CASE_SAVED_OBJECT, + name: `associated-${CASE_SAVED_OBJECT}`, + }); + } + + return references; + } } diff --git a/x-pack/plugins/cases/server/connectors/cases/constants.ts b/x-pack/plugins/cases/server/connectors/cases/constants.ts index 123eb6e0ac660..b65668f3c7a6b 100644 --- a/x-pack/plugins/cases/server/connectors/cases/constants.ts +++ b/x-pack/plugins/cases/server/connectors/cases/constants.ts @@ -9,6 +9,7 @@ import { CustomFieldTypes } from '../../../common/types/domain'; export const MAX_CONCURRENT_ES_REQUEST = 5; export const MAX_OPEN_CASES = 10; +export const DEFAULT_MAX_OPEN_CASES = 5; export const INITIAL_ORACLE_RECORD_COUNTER = 1; export const VALUES_FOR_CUSTOM_FIELDS_MISSING_DEFAULTS: Record = { diff --git a/x-pack/plugins/cases/server/connectors/cases/index.mock.ts b/x-pack/plugins/cases/server/connectors/cases/index.mock.ts index ca8b6c8f5a8b2..6bc5e69160b18 100644 --- a/x-pack/plugins/cases/server/connectors/cases/index.mock.ts +++ b/x-pack/plugins/cases/server/connectors/cases/index.mock.ts @@ -48,6 +48,41 @@ export const alerts = [ { _id: 'alert-id-4', _index: 'alert-index-4', 'host.name': 'A', 'source.ip': '0.0.0.5' }, ]; +export const alertsNested = [ + { + _id: 'alert-id-0', + _index: 'alert-index-0', + host: { name: 'A' }, + dest: { ip: '0.0.0.1' }, + source: { ip: '0.0.0.2' }, + }, + { + _id: 'alert-id-1', + _index: 'alert-index-1', + host: { name: 'B' }, + dest: { ip: '0.0.0.1' }, + file: { hash: '12345' }, + }, + { + _id: 'alert-id-2', + _index: 'alert-index-2', + host: { name: 'A' }, + dest: { ip: '0.0.0.1' }, + }, + { + _id: 'alert-id-3', + _index: 'alert-index-3', + host: { name: 'B' }, + dest: { ip: '0.0.0.3' }, + }, + { + _id: 'alert-id-4', + _index: 'alert-index-4', + host: { name: 'A' }, + source: { ip: '0.0.0.5' }, + }, +]; + export const groupingBy = ['host.name', 'dest.ip']; export const rule = { id: 'rule-test-id', 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 2b10829c22258..e5e40543f3149 100644 --- a/x-pack/plugins/cases/server/connectors/cases/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/cases/index.test.ts @@ -7,7 +7,8 @@ import type { SubActionConnectorType } from '@kbn/actions-plugin/server/sub_action_framework/types'; import type { CasesConnectorConfig, CasesConnectorSecrets } from './types'; -import { getCasesConnectorType } from '.'; +import { getCasesConnectorAdapter, getCasesConnectorType } from '.'; +import { AlertConsumers } from '@kbn/rule-data-utils'; describe('getCasesConnectorType', () => { let caseConnectorType: SubActionConnectorType; @@ -26,7 +27,16 @@ describe('getCasesConnectorType', () => { caseConnectorType.getKibanaPrivileges?.({ params: { subAction: 'run', subActionParams: { owner: 'my-owner' } }, }) - ).toEqual(['cases:my-owner/createCase', 'cases:my-owner/updateCase']); + ).toEqual([ + 'cases:my-owner/createCase', + 'cases:my-owner/updateCase', + 'cases:my-owner/deleteCase', + 'cases:my-owner/pushCase', + 'cases:my-owner/createComment', + 'cases:my-owner/updateComment', + 'cases:my-owner/deleteComment', + 'cases:my-owner/findConfigurations', + ]); }); it('throws if the owner is undefined', () => { @@ -35,4 +45,227 @@ describe('getCasesConnectorType', () => { ); }); }); + + describe('getCasesConnectorAdapter', () => { + const alerts = { + all: { + data: [ + { _id: 'alert-id-1', _index: 'alert-index-1' }, + { _id: 'alert-id-2', _index: 'alert-index-2' }, + ], + count: 2, + }, + new: { data: [{ _id: 'alert-id-1', _index: 'alert-index-1' }], count: 1 }, + ongoing: { data: [{ _id: 'alert-id-2', _index: 'alert-index-2' }], count: 1 }, + recovered: { data: [], count: 0 }, + }; + + const rule = { + id: 'rule-id', + name: 'my rule name', + tags: ['my-tag'], + consumer: 'test-consumer', + }; + + const getParams = (overrides = {}) => ({ + subAction: 'run' as const, + subActionParams: { groupingBy: [], reopenClosedCases: false, timeWindow: '7d', ...overrides }, + }); + + it('sets the correct connectorTypeId', () => { + const adapter = getCasesConnectorAdapter(); + + expect(adapter.connectorTypeId).toEqual('.cases'); + }); + + describe('ruleActionParamsSchema', () => { + it('validates getParams() correctly', () => { + const adapter = getCasesConnectorAdapter(); + + expect(adapter.ruleActionParamsSchema.validate(getParams())).toEqual(getParams()); + }); + + it('throws if missing getParams()', () => { + const adapter = getCasesConnectorAdapter(); + + expect(() => adapter.ruleActionParamsSchema.validate({})).toThrow(); + }); + + it('does not accept more than one groupingBy key', () => { + const adapter = getCasesConnectorAdapter(); + + expect(() => + adapter.ruleActionParamsSchema.validate( + getParams({ groupingBy: ['host.name', 'source.ip'] }) + ) + ).toThrow(); + }); + + it('should fail with not valid time window', () => { + const adapter = getCasesConnectorAdapter(); + + expect(() => + adapter.ruleActionParamsSchema.validate(getParams({ timeWindow: '10d+3d' })) + ).toThrow(); + }); + }); + + describe('buildActionParams', () => { + it('builds the action getParams() correctly', () => { + const adapter = getCasesConnectorAdapter(); + + expect( + adapter.buildActionParams({ + // @ts-expect-error: not all fields are needed + alerts, + rule, + params: getParams(), + spaceId: 'default', + ruleUrl: 'https://example.com', + }) + ).toMatchInlineSnapshot(` + Object { + "subAction": "run", + "subActionParams": Object { + "alerts": Array [ + Object { + "_id": "alert-id-1", + "_index": "alert-index-1", + }, + Object { + "_id": "alert-id-2", + "_index": "alert-index-2", + }, + ], + "groupingBy": Array [], + "maximumCasesToOpen": 5, + "owner": "cases", + "reopenClosedCases": false, + "rule": Object { + "id": "rule-id", + "name": "my rule name", + "ruleUrl": "https://example.com", + "tags": Array [ + "my-tag", + ], + }, + "timeWindow": "7d", + }, + } + `); + }); + + it('builds the action getParams() correctly without ruleUrl', () => { + const adapter = getCasesConnectorAdapter(); + expect( + adapter.buildActionParams({ + // @ts-expect-error: not all fields are needed + alerts, + rule, + params: getParams(), + spaceId: 'default', + }) + ).toMatchInlineSnapshot(` + Object { + "subAction": "run", + "subActionParams": Object { + "alerts": Array [ + Object { + "_id": "alert-id-1", + "_index": "alert-index-1", + }, + Object { + "_id": "alert-id-2", + "_index": "alert-index-2", + }, + ], + "groupingBy": Array [], + "maximumCasesToOpen": 5, + "owner": "cases", + "reopenClosedCases": false, + "rule": Object { + "id": "rule-id", + "name": "my rule name", + "ruleUrl": null, + "tags": Array [ + "my-tag", + ], + }, + "timeWindow": "7d", + }, + } + `); + }); + + it('maps observability consumers to the correct owner', () => { + const adapter = getCasesConnectorAdapter(); + + for (const consumer of [ + AlertConsumers.OBSERVABILITY, + AlertConsumers.APM, + AlertConsumers.INFRASTRUCTURE, + AlertConsumers.LOGS, + AlertConsumers.SLO, + AlertConsumers.UPTIME, + AlertConsumers.MONITORING, + ]) { + 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('observability'); + } + }); + + it('maps security solution consumers to the correct owner', () => { + const adapter = getCasesConnectorAdapter(); + + for (const consumer of [AlertConsumers.SIEM]) { + 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'); + } + }); + + it('maps stack consumers to the correct owner', () => { + const adapter = getCasesConnectorAdapter(); + + 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('cases'); + } + }); + + it('fallback to the cases owner if the consumer is not in the mapping', () => { + const adapter = getCasesConnectorAdapter(); + + const connectorParams = adapter.buildActionParams({ + // @ts-expect-error: not all fields are needed + alerts, + rule: { ...rule, consumer: 'not-valid' }, + params: getParams(), + spaceId: 'default', + }); + + expect(connectorParams.subActionParams.owner).toBe('cases'); + }); + }); + }); }); diff --git a/x-pack/plugins/cases/server/connectors/cases/index.ts b/x-pack/plugins/cases/server/connectors/cases/index.ts index 43235e8a90379..81a6125849274 100644 --- a/x-pack/plugins/cases/server/connectors/cases/index.ts +++ b/x-pack/plugins/cases/server/connectors/cases/index.ts @@ -5,18 +5,26 @@ * 2.0. */ -import { - AlertingConnectorFeatureId, - SecurityConnectorFeatureId, - UptimeConnectorFeatureId, -} from '@kbn/actions-plugin/common'; +import { AlertingConnectorFeatureId, UptimeConnectorFeatureId } from '@kbn/actions-plugin/common'; import type { SubActionConnectorType } from '@kbn/actions-plugin/server/sub_action_framework/types'; import type { KibanaRequest } from '@kbn/core-http-server'; import type { SavedObjectsClientContract } from '@kbn/core/server'; +import type { ConnectorAdapter } from '@kbn/alerting-plugin/server'; +import type { Owner } from '../../../common/constants/types'; import { CasesConnector } from './cases_connector'; -import { CASES_CONNECTOR_ID, CASES_CONNECTOR_TITLE } from '../../../common/constants'; -import type { CasesConnectorConfig, CasesConnectorSecrets } from './types'; -import { CasesConnectorConfigSchema, CasesConnectorSecretsSchema } from './schema'; +import { DEFAULT_MAX_OPEN_CASES } from './constants'; +import { CASES_CONNECTOR_ID, CASES_CONNECTOR_TITLE, OWNER_INFO } from '../../../common/constants'; +import type { + CasesConnectorConfig, + CasesConnectorParams, + CasesConnectorRuleActionParams, + CasesConnectorSecrets, +} from './types'; +import { + CasesConnectorConfigSchema, + CasesConnectorRuleActionParamsSchema, + CasesConnectorSecretsSchema, +} from './schema'; import type { CasesClient } from '../../client'; import { constructRequiredKibanaPrivileges } from './utils'; @@ -52,11 +60,7 @@ export const getCasesConnectorType = ({ * TODO: Limit only to rule types that support * alerts-as-data */ - supportedFeatureIds: [ - SecurityConnectorFeatureId, - UptimeConnectorFeatureId, - AlertingConnectorFeatureId, - ], + supportedFeatureIds: [UptimeConnectorFeatureId, AlertingConnectorFeatureId], /** * TODO: Verify license */ @@ -72,3 +76,44 @@ export const getCasesConnectorType = ({ return constructRequiredKibanaPrivileges(owner); }, }); + +export const getCasesConnectorAdapter = (): 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 = getOwnerFromRuleConsumer(rule.consumer); + + const subActionParams = { + alerts: caseAlerts, + rule: { id: rule.id, name: rule.name, tags: rule.tags, ruleUrl: ruleUrl ?? null }, + groupingBy: params.subActionParams.groupingBy, + owner, + reopenClosedCases: params.subActionParams.reopenClosedCases, + timeWindow: params.subActionParams.timeWindow, + maximumCasesToOpen: DEFAULT_MAX_OPEN_CASES, + }; + + return { subAction: 'run', subActionParams }; + }, + }; +}; + +const getOwnerFromRuleConsumer = (consumer: string): Owner => { + for (const value of Object.values(OWNER_INFO)) { + const foundedConsumer = value.validRuleConsumers?.find( + (validConsumer) => validConsumer === consumer + ); + + if (foundedConsumer) { + return value.id; + } + } + + return OWNER_INFO.cases.id; +}; diff --git a/x-pack/plugins/cases/server/connectors/cases/schema.ts b/x-pack/plugins/cases/server/connectors/cases/schema.ts index bf1fec00c806c..4cdc2a2d66993 100644 --- a/x-pack/plugins/cases/server/connectors/cases/schema.ts +++ b/x-pack/plugins/cases/server/connectors/cases/schema.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import dateMath from '@kbn/datemath'; -import { MAX_OPEN_CASES } from './constants'; +import { MAX_OPEN_CASES, DEFAULT_MAX_OPEN_CASES } from './constants'; import { CASES_CONNECTOR_TIME_WINDOW_REGEX } from '../../../common/constants'; const AlertSchema = schema.recordOf(schema.string(), schema.any(), { @@ -30,6 +30,33 @@ const RuleSchema = schema.object({ ruleUrl: schema.nullable(schema.string()), }); +const ReopenClosedCasesSchema = schema.boolean({ defaultValue: false }); +const TimeWindowSchema = schema.string({ + defaultValue: '7d', + validate: (value) => { + /** + * Validates the time window. + * Acceptable format: + * - First character should be a digit from 1 to 9 + * - All next characters should be a digit from 0 to 9 + * - The last character should be d (day) or w (week) or M (month) or Y (year) + * + * Example: 20d, 2w, 1M, etc + */ + const timeWindowRegex = new RegExp(CASES_CONNECTOR_TIME_WINDOW_REGEX, 'g'); + + if (!timeWindowRegex.test(value)) { + return 'Not a valid time window'; + } + + const date = dateMath.parse(`now-${value}`); + + if (!date || !date.isValid()) { + return 'Not a valid time window'; + } + }, +}); + /** * The case connector does not have any configuration * or secrets. @@ -42,31 +69,25 @@ export const CasesConnectorRunParamsSchema = schema.object({ groupingBy: GroupingSchema, owner: schema.string(), rule: RuleSchema, - timeWindow: schema.string({ - defaultValue: '7d', - validate: (value) => { - /** - * Validates the time window. - * Acceptable format: - * - First character should be a digit from 1 to 9 - * - All next characters should be a digit from 0 to 9 - * - The last character should be d (day) or w (week) or M (month) or Y (year) - * - * Example: 20d, 2w, 1M, etc - */ - const timeWindowRegex = new RegExp(CASES_CONNECTOR_TIME_WINDOW_REGEX, 'g'); - - if (!timeWindowRegex.test(value)) { - return 'Not a valid time window'; - } - - const date = dateMath.parse(`now-${value}`); + timeWindow: TimeWindowSchema, + reopenClosedCases: ReopenClosedCasesSchema, + maximumCasesToOpen: schema.number({ + defaultValue: DEFAULT_MAX_OPEN_CASES, + min: 1, + max: MAX_OPEN_CASES, + }), +}); - if (!date || !date.isValid()) { - return 'Not a valid time window'; - } - }, +export const CasesConnectorRuleActionParamsSchema = schema.object({ + subAction: schema.literal('run'), + subActionParams: schema.object({ + groupingBy: GroupingSchema, + reopenClosedCases: ReopenClosedCasesSchema, + timeWindow: TimeWindowSchema, }), - reopenClosedCases: schema.boolean({ defaultValue: false }), - maximumCasesToOpen: schema.number({ defaultValue: 5, min: 1, max: MAX_OPEN_CASES }), +}); + +export const CasesConnectorParamsSchema = schema.object({ + subAction: schema.literal('run'), + subActionParams: CasesConnectorRunParamsSchema, }); diff --git a/x-pack/plugins/cases/server/connectors/cases/types.ts b/x-pack/plugins/cases/server/connectors/cases/types.ts index 11cda9bff1477..7f811de899fea 100644 --- a/x-pack/plugins/cases/server/connectors/cases/types.ts +++ b/x-pack/plugins/cases/server/connectors/cases/types.ts @@ -13,6 +13,8 @@ import type { CasesConnectorConfigSchema, CasesConnectorSecretsSchema, CasesConnectorRunParamsSchema, + CasesConnectorRuleActionParamsSchema, + CasesConnectorParamsSchema, } from './schema'; export type CasesConnectorConfig = TypeOf; @@ -86,3 +88,6 @@ export interface BackoffStrategy { export interface BackoffFactory { create: () => BackoffStrategy; } + +export type CasesConnectorRuleActionParams = TypeOf; +export type CasesConnectorParams = TypeOf; diff --git a/x-pack/plugins/cases/server/connectors/index.ts b/x-pack/plugins/cases/server/connectors/index.ts index 0baf7f87f0dad..2d680163dde28 100644 --- a/x-pack/plugins/cases/server/connectors/index.ts +++ b/x-pack/plugins/cases/server/connectors/index.ts @@ -9,19 +9,22 @@ import type { PluginSetupContract as ActionsPluginSetupContract } from '@kbn/act import type { KibanaRequest } from '@kbn/core-http-server'; import type { CoreSetup, SavedObjectsClientContract } from '@kbn/core/server'; import { SECURITY_EXTENSION_ID } from '@kbn/core/server'; +import type { PluginSetupContract as AlertingPluginSetup } from '@kbn/alerting-plugin/server'; import type { CasesClient } from '../client'; -import { getCasesConnectorType } from './cases'; +import { getCasesConnectorAdapter, getCasesConnectorType } from './cases'; export * from './types'; export { casesConnectors } from './factory'; export function registerConnectorTypes({ + alerting, actions, core, getCasesClient, getSpaceId, }: { actions: ActionsPluginSetupContract; + alerting: AlertingPluginSetup; core: CoreSetup; getCasesClient: (request: KibanaRequest) => Promise; getSpaceId: (request?: KibanaRequest) => string; @@ -52,4 +55,6 @@ export function registerConnectorTypes({ actions.registerSubActionConnectorType( getCasesConnectorType({ getCasesClient, getSpaceId, getUnsecuredSavedObjectsClient }) ); + + alerting.registerConnectorAdapter(getCasesConnectorAdapter()); } diff --git a/x-pack/plugins/cases/server/plugin.test.ts b/x-pack/plugins/cases/server/plugin.test.ts index ad852fc65ebc5..8c669f6de0e68 100644 --- a/x-pack/plugins/cases/server/plugin.test.ts +++ b/x-pack/plugins/cases/server/plugin.test.ts @@ -48,6 +48,7 @@ describe('Cases Plugin', () => { coreStart = coreMock.createStart(); pluginsSetup = { + alerting: alertsMock.createSetup(), taskManager: taskManagerMock.createSetup(), actions: actionsMock.createSetup(), files: createFilesSetupMock(), diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 86ac9084b7c7c..2c3f1f10ad254 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -145,7 +145,13 @@ export class CasePlugin return plugins.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; }; - registerConnectorTypes({ actions: plugins.actions, core, getCasesClient, getSpaceId }); + registerConnectorTypes({ + actions: plugins.actions, + alerting: plugins.alerting, + core, + getCasesClient, + getSpaceId, + }); return { attachmentFramework: { diff --git a/x-pack/plugins/cases/server/types.ts b/x-pack/plugins/cases/server/types.ts index cec7e313690c4..f404f7346144c 100644 --- a/x-pack/plugins/cases/server/types.ts +++ b/x-pack/plugins/cases/server/types.ts @@ -32,12 +32,14 @@ import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import type { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/server'; 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 { CasesClient } from './client'; import type { AttachmentFramework } from './attachment_framework/types'; import type { ExternalReferenceAttachmentTypeRegistry } from './attachment_framework/external_reference_registry'; import type { PersistableStateAttachmentTypeRegistry } from './attachment_framework/persistable_state_registry'; export interface CasesServerSetupDependencies { + alerting: AlertingPluginSetup; actions: ActionsPluginSetup; lens: LensServerPluginSetup; features: FeaturesPluginSetup; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions.tsx index a4e0ea59e685c..d8d01470fa9c9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { EuiText, EuiSpacer, @@ -52,18 +52,20 @@ export function RuleActions({ ); } - const getNotifyText = (action: RuleUiAction, isSystemAction?: boolean) => { + const getNotifyText = (action: RuleUiAction, isSystemAction?: boolean): string | ReactNode => { if (isSystemAction) { return NOTIFY_WHEN_OPTIONS[1].inputDisplay; } - return ( - ('frequency' in action && - (NOTIFY_WHEN_OPTIONS.find((options) => options.value === action.frequency?.notifyWhen) - ?.inputDisplay || - action.frequency?.notifyWhen)) ?? - legacyNotifyWhen - ); + if ('frequency' in action) { + const notifyWhen = NOTIFY_WHEN_OPTIONS.find( + (options) => options.value === action.frequency?.notifyWhen + ); + + return notifyWhen?.inputDisplay ?? action.frequency?.notifyWhen ?? legacyNotifyWhen ?? ''; + } + + return ''; }; const getActionIconClass = (actionGroupId?: string): IconType | undefined => { @@ -85,6 +87,7 @@ export function RuleActions({ {ruleActions.map((action, index) => { const { actionTypeId, id } = action; const actionName = getActionName(id); + return ( @@ -105,7 +108,9 @@ export function RuleActions({ {String( diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/bulk_enqueue.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/bulk_enqueue.ts index 599f370122358..acf6e86652862 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/bulk_enqueue.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/bulk_enqueue.ts @@ -100,7 +100,7 @@ export default function ({ getService }: FtrProviderContext) { const startDate = new Date().toISOString(); const connectorId = 'system-connector-test.system-action-kibana-privileges'; - const name = 'System action: test.system-action-kibana-privileges'; + const name = 'Test system action with kibana privileges'; const reference = `actions-enqueue-${scenario.id}:${space.id}:${connectorId}`; const response = await supertestWithoutAuth diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/execute.ts index 448296a4ae00c..acfb06e64cff6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/execute.ts @@ -508,7 +508,7 @@ export default function ({ getService }: FtrProviderContext) { it('should authorize system actions correctly', async () => { const startDate = new Date().toISOString(); const connectorId = 'system-connector-test.system-action-kibana-privileges'; - const name = 'System action: test.system-action-kibana-privileges'; + const name = 'Test system action with kibana privileges'; const reference = `actions-enqueue-${scenario.id}:${space.id}:${connectorId}`; /** diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all_system.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all_system.ts index 2811c7e2d4ce2..75f34b1458bd1 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all_system.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all_system.ts @@ -68,6 +68,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { (conn: { id: string }) => !conn.id.startsWith('custom.ssl.') ); expect(nonCustomSslConnectors).to.eql([ + { + connector_type_id: '.cases', + id: 'system-connector-.cases', + is_deprecated: false, + is_preconfigured: false, + is_system_action: true, + name: 'Cases', + referenced_by_count: 0, + }, { id: createdAction.id, is_preconfigured: false, @@ -126,13 +135,22 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Slack#xyz', referenced_by_count: 0, }, + { + id: 'custom-system-abc-connector', + is_preconfigured: true, + is_system_action: false, + is_deprecated: false, + connector_type_id: 'system-abc-action-type', + name: 'SystemABC', + referenced_by_count: 0, + }, { connector_type_id: 'test.system-action', id: 'system-connector-test.system-action', is_deprecated: false, is_preconfigured: false, is_system_action: true, - name: 'System action: test.system-action', + name: 'Test system action', referenced_by_count: 0, }, { @@ -141,7 +159,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { is_deprecated: false, is_preconfigured: false, is_system_action: true, - name: 'System action: test.system-action-connector-adapter', + name: 'Test system action with a connector adapter set', referenced_by_count: 0, }, { @@ -150,16 +168,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { is_deprecated: false, is_preconfigured: false, is_system_action: true, - name: 'System action: test.system-action-kibana-privileges', - referenced_by_count: 0, - }, - { - id: 'custom-system-abc-connector', - is_preconfigured: true, - is_system_action: false, - is_deprecated: false, - connector_type_id: 'system-abc-action-type', - name: 'SystemABC', + name: 'Test system action with kibana privileges', referenced_by_count: 0, }, { @@ -255,6 +264,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { (conn: { id: string }) => !conn.id.startsWith('custom.ssl.') ); expect(nonCustomSslConnectors).to.eql([ + { + connector_type_id: '.cases', + id: 'system-connector-.cases', + is_deprecated: false, + is_preconfigured: false, + is_system_action: true, + name: 'Cases', + referenced_by_count: 0, + }, { id: createdAction.id, is_preconfigured: false, @@ -313,13 +331,22 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Slack#xyz', referenced_by_count: 0, }, + { + id: 'custom-system-abc-connector', + is_preconfigured: true, + is_system_action: false, + is_deprecated: false, + connector_type_id: 'system-abc-action-type', + name: 'SystemABC', + referenced_by_count: 0, + }, { connector_type_id: 'test.system-action', id: 'system-connector-test.system-action', is_deprecated: false, is_preconfigured: false, is_system_action: true, - name: 'System action: test.system-action', + name: 'Test system action', referenced_by_count: 0, }, { @@ -328,7 +355,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { is_deprecated: false, is_preconfigured: false, is_system_action: true, - name: 'System action: test.system-action-connector-adapter', + name: 'Test system action with a connector adapter set', referenced_by_count: 0, }, { @@ -337,18 +364,10 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { is_deprecated: false, is_preconfigured: false, is_system_action: true, - name: 'System action: test.system-action-kibana-privileges', - referenced_by_count: 0, - }, - { - id: 'custom-system-abc-connector', - is_preconfigured: true, - is_system_action: false, - is_deprecated: false, - connector_type_id: 'system-abc-action-type', - name: 'SystemABC', + name: 'Test system action with kibana privileges', referenced_by_count: 0, }, + { id: 'preconfigured.test.index-record', is_preconfigured: true, @@ -418,6 +437,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { (conn: { id: string }) => !conn.id.startsWith('custom.ssl.') ); expect(nonCustomSslConnectors).to.eql([ + { + connector_type_id: '.cases', + id: 'system-connector-.cases', + is_deprecated: false, + is_preconfigured: false, + is_system_action: true, + name: 'Cases', + referenced_by_count: 0, + }, { connector_type_id: '.email', id: 'notification-email', @@ -463,13 +491,22 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Slack#xyz', referenced_by_count: 0, }, + { + id: 'custom-system-abc-connector', + is_preconfigured: true, + is_system_action: false, + is_deprecated: false, + connector_type_id: 'system-abc-action-type', + name: 'SystemABC', + referenced_by_count: 0, + }, { connector_type_id: 'test.system-action', id: 'system-connector-test.system-action', is_deprecated: false, is_preconfigured: false, is_system_action: true, - name: 'System action: test.system-action', + name: 'Test system action', referenced_by_count: 0, }, { @@ -478,7 +515,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { is_deprecated: false, is_preconfigured: false, is_system_action: true, - name: 'System action: test.system-action-connector-adapter', + name: 'Test system action with a connector adapter set', referenced_by_count: 0, }, { @@ -487,16 +524,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { is_deprecated: false, is_preconfigured: false, is_system_action: true, - name: 'System action: test.system-action-kibana-privileges', - referenced_by_count: 0, - }, - { - id: 'custom-system-abc-connector', - is_preconfigured: true, - is_system_action: false, - is_deprecated: false, - connector_type_id: 'system-abc-action-type', - name: 'SystemABC', + name: 'Test system action with kibana privileges', referenced_by_count: 0, }, { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/alerts.ts index 34f07f70f0216..9d3cac9ef9a6d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/alerts.ts @@ -1880,7 +1880,7 @@ instanceStateValue: true const space = SuperuserAtSpace1.space; const connectorId = 'system-connector-test.system-action-connector-adapter'; - const name = 'System action: test.system-action-connector-adapter'; + const name = 'Test system action with a connector adapter set'; it('should use connector adapters correctly on system actions', async () => { const alertUtils = new AlertUtils({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index 45d469eb1132d..17187505aa2ad 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -342,7 +342,7 @@ export default function ({ getService }: FtrProviderContext) { */ it('should execute system actions correctly', async () => { const connectorId = 'system-connector-test.system-action'; - const name = 'System action: test.system-action'; + const name = 'Test system action'; const response = await supertest .post( @@ -375,7 +375,7 @@ export default function ({ getService }: FtrProviderContext) { */ it('should execute system actions with kibana privileges correctly', async () => { const connectorId = 'system-connector-test.system-action-kibana-privileges'; - const name = 'System action: test.system-action-kibana-privileges'; + const name = 'Test system action with kibana privileges'; const response = await supertest .post( diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all_system.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all_system.ts index 014a894db7d31..2690ff487ff5d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all_system.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all_system.ts @@ -56,6 +56,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { is_system_action: false, referenced_by_count: 0, }, + { + connector_type_id: '.cases', + id: 'system-connector-.cases', + is_deprecated: false, + is_preconfigured: false, + is_system_action: true, + name: 'Cases', + referenced_by_count: 0, + }, { id: createdAction.id, is_preconfigured: false, @@ -114,13 +123,22 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Slack#xyz', referenced_by_count: 0, }, + { + id: 'custom-system-abc-connector', + is_preconfigured: true, + is_deprecated: false, + connector_type_id: 'system-abc-action-type', + is_system_action: false, + name: 'SystemABC', + referenced_by_count: 0, + }, { connector_type_id: 'test.system-action', id: 'system-connector-test.system-action', is_deprecated: false, is_preconfigured: false, is_system_action: true, - name: 'System action: test.system-action', + name: 'Test system action', referenced_by_count: 0, }, { @@ -129,7 +147,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { is_deprecated: false, is_preconfigured: false, is_system_action: true, - name: 'System action: test.system-action-connector-adapter', + name: 'Test system action with a connector adapter set', referenced_by_count: 0, }, { @@ -138,18 +156,10 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { is_deprecated: false, is_preconfigured: false, is_system_action: true, - name: 'System action: test.system-action-kibana-privileges', - referenced_by_count: 0, - }, - { - id: 'custom-system-abc-connector', - is_preconfigured: true, - is_deprecated: false, - connector_type_id: 'system-abc-action-type', - is_system_action: false, - name: 'SystemABC', + name: 'Test system action with kibana privileges', referenced_by_count: 0, }, + { id: 'preconfigured.test.index-record', is_preconfigured: true, @@ -208,6 +218,15 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { is_system_action: false, referenced_by_count: 0, }, + { + connector_type_id: '.cases', + id: 'system-connector-.cases', + is_deprecated: false, + is_preconfigured: false, + is_system_action: true, + name: 'Cases', + referenced_by_count: 0, + }, { connector_type_id: '.email', id: 'notification-email', @@ -253,13 +272,22 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { name: 'Slack#xyz', referenced_by_count: 0, }, + { + id: 'custom-system-abc-connector', + is_preconfigured: true, + is_deprecated: false, + is_system_action: false, + connector_type_id: 'system-abc-action-type', + name: 'SystemABC', + referenced_by_count: 0, + }, { connector_type_id: 'test.system-action', id: 'system-connector-test.system-action', is_deprecated: false, is_preconfigured: false, is_system_action: true, - name: 'System action: test.system-action', + name: 'Test system action', referenced_by_count: 0, }, { @@ -268,7 +296,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { is_deprecated: false, is_preconfigured: false, is_system_action: true, - name: 'System action: test.system-action-connector-adapter', + name: 'Test system action with a connector adapter set', referenced_by_count: 0, }, { @@ -277,16 +305,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { is_deprecated: false, is_preconfigured: false, is_system_action: true, - name: 'System action: test.system-action-kibana-privileges', - referenced_by_count: 0, - }, - { - id: 'custom-system-abc-connector', - is_preconfigured: true, - is_deprecated: false, - is_system_action: false, - connector_type_id: 'system-abc-action-type', - name: 'SystemABC', + name: 'Test system action with kibana privileges', referenced_by_count: 0, }, { diff --git a/x-pack/test/cases_api_integration/common/lib/api/connectors.ts b/x-pack/test/cases_api_integration/common/lib/api/connectors.ts index 16d7a2deb1b90..0eb6854cb735d 100644 --- a/x-pack/test/cases_api_integration/common/lib/api/connectors.ts +++ b/x-pack/test/cases_api_integration/common/lib/api/connectors.ts @@ -344,3 +344,26 @@ export const executeConnector = async ({ return mapKeys(res, (_v, k) => camelCase(k)) as ActionTypeExecutorResult; }; + +export const executeSystemConnector = async ({ + supertest, + connectorId, + req, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + connectorId: string; + req: Record; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise> => { + const { body: res } = await supertest + .post(`${getSpaceUrlPrefix(auth.space)}/api/cases_fixture/${connectorId}/connectors:execute`) + .auth(auth.user.username, auth.user.password) + .set('kbn-xsrf', 'true') + .send(req) + .expect(expectedHttpCode); + + return mapKeys(res, (_v, k) => camelCase(k)) as ActionTypeExecutorResult; +}; diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc b/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc index 3989d35f8a2aa..135db481efeef 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc +++ b/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc @@ -7,6 +7,7 @@ "server": true, "browser": false, "requiredPlugins": [ + "actions", "features", "cases", "files", diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts b/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts index 9de0d8b2a8cef..8d5ee1660d1fd 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts +++ b/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts @@ -11,6 +11,7 @@ import { SpacesPluginStart } from '@kbn/spaces-plugin/server'; import { SecurityPluginStart } from '@kbn/security-plugin/server'; import type { CasesServerStart, CasesServerSetup } from '@kbn/cases-plugin/server'; import { FilesSetup } from '@kbn/files-plugin/server'; +import { PluginStartContract as ActionsPluginsStart } from '@kbn/actions-plugin/server/plugin'; import { getPersistableStateAttachment } from './attachments/persistable_state'; import { getExternalReferenceAttachment } from './attachments/external_reference'; import { registerRoutes } from './routes'; @@ -23,6 +24,7 @@ export interface FixtureSetupDeps { } export interface FixtureStartDeps { + actions: ActionsPluginsStart; security?: SecurityPluginStart; spaces?: SpacesPluginStart; cases: CasesServerStart; diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts b/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts index 6345bae753f3f..11335c4d7adc7 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts +++ b/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts @@ -14,6 +14,7 @@ import type { PersistableStateAttachmentTypeSetup, } from '@kbn/cases-plugin/server/attachment_framework/types'; import { BulkCreateCasesRequest, CasesPatchRequest } from '@kbn/cases-plugin/common/types/api'; +import { ActionExecutionSourceType } from '@kbn/actions-plugin/server/types'; import type { FixtureStartDeps } from './plugin'; const hashParts = (parts: string[]): string => { @@ -138,4 +139,43 @@ export const registerRoutes = (core: CoreSetup, logger: Logger } } ); + + router.post( + { + path: '/api/cases_fixture/{id}/connectors:execute', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: schema.object({ + params: schema.recordOf(schema.string(), schema.any()), + }), + }, + }, + async (context, req, res) => { + const [_, { actions }] = await core.getStartServices(); + + const actionsClient = await actions.getActionsClientWithRequest(req); + + try { + return res.ok({ + body: await actionsClient.execute({ + actionId: req.params.id, + params: req.body.params, + source: { + type: ActionExecutionSourceType.HTTP_REQUEST, + source: req, + }, + relatedSavedObjects: [], + }), + }); + } catch (err) { + if (err.isBoom && err.output.statusCode === 403) { + return res.forbidden({ body: err }); + } + + throw err; + } + } + ); }; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/connectors/cases/cases_connector.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/connectors/cases/cases_connector.ts index 78fe0f104f0f5..a6e225f4d7373 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/connectors/cases/cases_connector.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/connectors/cases/cases_connector.ts @@ -39,7 +39,7 @@ import { } from '../../../../../common/lib/authentication/users'; import { deleteAllCaseItems, - executeConnector, + executeSystemConnector, findCases, getAllComments, updateCase, @@ -81,7 +81,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('validation', () => { it('returns 400 if the alerts do not contain _id and _index', async () => { - const res = await executeConnector({ + const res = await executeSystemConnector({ supertest, connectorId, // @ts-expect-error: need to test schema validation @@ -95,7 +95,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('returns 400 when groupingBy has more than one value', async () => { - const res = await executeConnector({ + const res = await executeSystemConnector({ supertest, connectorId, req: getRequest({ groupingBy: ['host.name', 'source.ip'] }), @@ -108,7 +108,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('returns 400 when timeWindow is invalid', async () => { - const res = await executeConnector({ + const res = await executeSystemConnector({ supertest, connectorId, req: getRequest({ timeWindow: 'not-valid' }), @@ -121,7 +121,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('returns 400 for valid date math but not valid time window', async () => { - const res = await executeConnector({ + const res = await executeSystemConnector({ supertest, connectorId, req: getRequest({ timeWindow: '10d+3d' }), @@ -135,7 +135,7 @@ export default ({ getService }: FtrProviderContext): void => { it('returns 400 for unsupported time units', async () => { for (const unit of ['s', 'm', 'H', 'h']) { - const res = await executeConnector({ + const res = await executeSystemConnector({ supertest, connectorId, req: getRequest({ timeWindow: `5${unit}` }), @@ -149,7 +149,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('returns 400 when maximumCasesToOpen > 10', async () => { - const res = await executeConnector({ + const res = await executeSystemConnector({ supertest, connectorId, req: getRequest({ maximumCasesToOpen: 11 }), @@ -162,7 +162,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('returns 400 when maximumCasesToOpen < 1', async () => { - const res = await executeConnector({ + const res = await executeSystemConnector({ supertest, connectorId, req: getRequest({ maximumCasesToOpen: 0 }), @@ -968,7 +968,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should not execute without permission to cases for all owners', async () => { for (const owner of ['cases', 'securitySolution', 'observability']) { const req = getRequest({ owner }); - await executeConnector({ + await executeSystemConnector({ supertest: supertestWithoutAuth, connectorId, req, @@ -980,7 +980,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should not execute in a space with no permissions', async () => { const req = getRequest({ owner: 'securitySolution' }); - await executeConnector({ + await executeSystemConnector({ supertest: supertestWithoutAuth, connectorId, req, @@ -998,7 +998,7 @@ export default ({ getService }: FtrProviderContext): void => { noKibanaPrivileges, ]) { const req = getRequest({ owner: 'securitySolution' }); - await executeConnector({ + await executeSystemConnector({ supertest: supertestWithoutAuth, connectorId, req, @@ -1019,7 +1019,7 @@ export default ({ getService }: FtrProviderContext): void => { for (const [user, owner] of usersToTest) { const req = getRequest({ owner }); - const res = await executeConnector({ + const res = await executeSystemConnector({ supertest: supertestWithoutAuth, connectorId, req, @@ -1042,7 +1042,7 @@ export default ({ getService }: FtrProviderContext): void => { for (const [user, owner] of usersToTest) { const req = getRequest({ owner }); - await executeConnector({ + await executeSystemConnector({ supertest: supertestWithoutAuth, connectorId, req, @@ -1279,7 +1279,7 @@ const executeConnectorAndVerifyCorrectness = async ({ connectorId: string; req: Record; }) => { - const res = await executeConnector({ supertest, connectorId, req }); + const res = await executeSystemConnector({ supertest, connectorId, req }); expect(res.status).to.be('ok');