Skip to content

Commit

Permalink
[Cases] Register connector adapter (#179796)
Browse files Browse the repository at this point in the history
## Summary

Now that the system actions PR is merged
(#166267) we can use the connector
adapters to transform the case action params. This PR:

- Registers a connector adapter for the case action.
- Uses flattened objects in the description and the tags.
- Change the integration tests to use an internal router to execute
system actions. PR #166267
disabled execution of system actions through the public execute API.
- Skip execution of the case action if the grouping did not produce any
alerts.
- Add references to the cases oracle saved objects.
- Remove the owner from the UI and deduct the owner from the rule's
consumer in the connector adapter.

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
  • Loading branch information
cnasikas authored Apr 4, 2024
1 parent 44b9d94 commit aec669d
Show file tree
Hide file tree
Showing 39 changed files with 778 additions and 203 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/actions/server/create_system_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/actions/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ describe('Actions Plugin', () => {
{
id: 'system-connector-.cases',
actionTypeId: '.cases',
name: 'System action: .cases',
name: 'Cases',
config: {},
secrets: {},
isDeprecated: false,
Expand Down Expand Up @@ -769,7 +769,7 @@ describe('Actions Plugin', () => {
{
id: 'system-connector-.cases',
actionTypeId: '.cases',
name: 'System action: .cases',
name: 'Cases',
config: {},
secrets: {},
isDeprecated: false,
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/alerting/server/connector_adapters/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ObjectType } from '@kbn/config-schema';
import type { RuleTypeParams, SanitizedRule } from '../../common';
import { CombinedSummarizedAlerts } from '../types';

type Rule = Pick<SanitizedRule<RuleTypeParams>, 'id' | 'name' | 'tags'>;
type Rule = Pick<SanitizedRule<RuleTypeParams>, 'id' | 'name' | 'tags' | 'consumer'>;

export interface ConnectorAdapterParams {
[x: string]: unknown;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ const rule = {
uuid: '111-111',
},
],
consumer: 'test-consumer',
} as unknown as SanitizedRule<RuleTypeParams>;

const defaultExecutionParams = {
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions x-pack/plugins/cases/common/constants/owner.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
14 changes: 14 additions & 0 deletions x-pack/plugins/cases/common/constants/owners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

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

Expand All @@ -23,6 +24,7 @@ interface RouteInfo {
label: string;
iconType: string;
appRoute: string;
validRuleConsumers?: readonly AlertConsumers[];
}

export const OWNER_INFO: Record<Owner, RouteInfo> = {
Expand All @@ -32,19 +34,31 @@ export const OWNER_INFO: Record<Owner, RouteInfo> = {
label: 'Security',
iconType: 'logoSecurity',
appRoute: '/app/security',
validRuleConsumers: [AlertConsumers.SIEM],
},
[OBSERVABILITY_OWNER]: {
id: OBSERVABILITY_OWNER,
appId: 'observability-overview',
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,
appId: 'management',
label: 'Stack',
iconType: 'casesApp',
appRoute: '/app/management/insightsAndAlerting',
validRuleConsumers: [AlertConsumers.ML, AlertConsumers.STACK_ALERTS, AlertConsumers.EXAMPLE],
},
} as const;
1 change: 1 addition & 0 deletions x-pack/plugins/cases/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"cases"
],
"requiredPlugins": [
"alerting",
"actions",
"data",
"embeddable",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,6 @@ export function getConnectorType(): ConnectorTypeModel<{}, {}, CasesActionParams
return validationResult;
},
actionParamsFields: lazy(() => import('./cases_params')),
isSystemActionType: true,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ const useApplicationMock = useApplication as jest.Mock;
const actionParams = {
subAction: 'run',
subActionParams: {
owner: 'cases',
timeWindow: '6w',
reopenClosedCases: false,
groupingBy: [],
Expand Down Expand Up @@ -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(<CasesParamsFields {...newProps} />);

expect(editAction.mock.calls[0][1].owner).toEqual('cases');
});

it('If timeWindow has errors, form row is invalid', async () => {
const newProps = {
...defaultProps,
Expand All @@ -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(<CasesParamsFields {...newProps} />);

expect(editAction.mock.calls[0][1].owner).toEqual('cases');

rerender(<CasesParamsFields {...defaultProps} />);

expect(editAction.mock.calls[1][1].owner).toEqual('securitySolution');
});

describe('UI updates', () => {
it('renders grouping by field options', async () => {
render(<CasesParamsFields {...defaultProps} />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -32,9 +30,6 @@ import { useAlertDataViews } from '../hooks/use_alert_data_view';
export const CasesParamsFieldsComponent: React.FunctionComponent<
ActionParamsProps<CasesActionParams>
> = ({ actionParams, editAction, errors, index, producerId }) => {
const { appId } = useApplication();
const owner = getCaseOwnerByAppId(appId);

const { dataViews, loading: loadingAlertDataViews } = useAlertDataViews(
producerId ? [producerId as ValidFeatureId] : []
);
Expand Down Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export interface CasesSubActionParamsUI {
timeWindow: string;
reopenClosedCases: boolean;
groupingBy: string[];
owner: string;
}
export interface CasesActionParams {
subAction: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
timeWindow,
reopenClosedCases,
updatedCounterOracleRecord,
alertsNested,
} from './index.mock';
import {
expectCasesToHaveTheCorrectAlertsAttachedWithGrouping,
Expand Down Expand Up @@ -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`'
);
});

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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 }],
Expand Down Expand Up @@ -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();
});
});
});
});

Expand Down
Loading

0 comments on commit aec669d

Please sign in to comment.