[Security Solution] [Detections] Adds support for system actions (and cases action) to detection rules (#183937)

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com>
This commit is contained in:
Devin W. Hurley 2024-07-26 13:20:39 -04:00 committed by GitHub
parent 1aee15f611
commit d7493052e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 1302 additions and 128 deletions

View file

@ -5,7 +5,11 @@
* 2.0.
*/
import { AlertingConnectorFeatureId, UptimeConnectorFeatureId } from '@kbn/actions-plugin/common';
import {
AlertingConnectorFeatureId,
UptimeConnectorFeatureId,
SecurityConnectorFeatureId,
} 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';
@ -56,7 +60,11 @@ export const getCasesConnectorType = ({
config: CasesConnectorConfigSchema,
secrets: CasesConnectorSecretsSchema,
},
supportedFeatureIds: [UptimeConnectorFeatureId, AlertingConnectorFeatureId],
supportedFeatureIds: [
UptimeConnectorFeatureId,
AlertingConnectorFeatureId,
SecurityConnectorFeatureId,
],
minimumLicenseRequired: 'platinum' as const,
isSystemActionType: true,
getKibanaPrivileges: ({ params } = { params: { subAction: 'run', subActionParams: {} } }) => {

View file

@ -514,7 +514,7 @@ export const RuleAction = z.object({
* The action type used for sending notifications.
*/
action_type_id: z.string(),
group: RuleActionGroup,
group: RuleActionGroup.optional(),
id: RuleActionId,
params: RuleActionParams,
uuid: NonEmptyString.optional(),

View file

@ -543,7 +543,6 @@ components:
$ref: '#/components/schemas/RuleActionFrequency'
required:
- action_type_id
- group
- id
- params

View file

@ -719,17 +719,6 @@ describe('rules schema', () => {
);
});
test('You cannot send in an array of actions that are missing "group"', () => {
const payload = {
...getCreateRulesSchemaMock(),
actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }],
};
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('actions.0.group: Required');
});
test('You cannot send in an array of actions that are missing "id"', () => {
const payload = {
...getCreateRulesSchemaMock(),

View file

@ -218,7 +218,7 @@ export const BulkActionEditTypeEnum = BulkActionEditType.enum;
export type NormalizedRuleAction = z.infer<typeof NormalizedRuleAction>;
export const NormalizedRuleAction = z
.object({
group: RuleActionGroup,
group: RuleActionGroup.optional(),
id: RuleActionId,
params: RuleActionParams,
frequency: RuleActionFrequency.optional(),

View file

@ -328,7 +328,6 @@ components:
alerts_filter:
$ref: '../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/RuleActionAlertsFilter'
required:
- group
- id
- params
additionalProperties: false

View file

@ -766,19 +766,6 @@ describe('Patch rule request schema', () => {
});
});
test('You cannot send in an array of actions that are missing "group"', () => {
const payload: Omit<PatchRuleRequestBody['actions'], 'group'> = {
...getPatchRulesSchemaMock(),
actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }],
};
const result = PatchRuleRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"actions.0.group: Required, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", actions.0.group: Required, actions.0.group: Required, and 12 more"`
);
});
test('You cannot send in an array of actions that are missing "id"', () => {
const payload: Omit<PatchRuleRequestBody['actions'], 'id'> = {
...getPatchRulesSchemaMock(),

View file

@ -776,20 +776,6 @@ describe('RuleToImport', () => {
expectParseSuccess(result);
});
test('You cannot send in an array of actions that are missing "group"', () => {
const payload = getImportRulesSchemaMock({
actions: [
// @ts-expect-error assign unsupported value
{ id: 'id', action_type_id: 'action_type_id', params: {} },
],
});
const result = RuleToImport.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"actions.0.group: Required"`);
});
test('You cannot send in an array of actions that are missing "id"', () => {
const payload = getImportRulesSchemaMock({
actions: [

View file

@ -5,8 +5,14 @@
* 2.0.
*/
import type { RuleAction as AlertingRuleAction } from '@kbn/alerting-plugin/common';
import type { NormalizedAlertAction } from '@kbn/alerting-plugin/server/rules_client';
import type {
RuleAction as AlertingRuleAction,
RuleSystemAction as AlertingRuleSystemAction,
} from '@kbn/alerting-plugin/common';
import type {
NormalizedAlertAction,
NormalizedSystemAction,
} from '@kbn/alerting-plugin/server/rules_client';
import type { NormalizedRuleAction } from '../api/detection_engine/rule_management';
import type {
ResponseAction,
@ -23,8 +29,7 @@ export const transformRuleToAlertAction = ({
uuid,
frequency,
alerts_filter: alertsFilter,
}: RuleAction): AlertingRuleAction => ({
group,
}: RuleAction): AlertingRuleAction | AlertingRuleSystemAction => ({
id,
params: params as AlertingRuleAction['params'],
actionTypeId,
@ -33,6 +38,7 @@ export const transformRuleToAlertAction = ({
}),
...(uuid && { uuid }),
...(frequency && { frequency }),
...(group && { group }),
});
export const transformAlertToRuleAction = ({
@ -44,13 +50,25 @@ export const transformAlertToRuleAction = ({
frequency,
alertsFilter,
}: AlertingRuleAction): RuleAction => ({
group,
id,
params,
action_type_id: actionTypeId,
...(alertsFilter && { alerts_filter: alertsFilter }),
...(uuid && { uuid }),
...(frequency && { frequency }),
...(group && { group }),
});
export const transformAlertToRuleSystemAction = ({
id,
actionTypeId,
params,
uuid,
}: AlertingRuleSystemAction): RuleAction => ({
id,
params,
action_type_id: actionTypeId,
...(uuid && { uuid }),
});
export const transformNormalizedRuleToAlertAction = ({
@ -59,8 +77,7 @@ export const transformNormalizedRuleToAlertAction = ({
params,
frequency,
alerts_filter: alertsFilter,
}: NormalizedRuleAction): NormalizedAlertAction => ({
group,
}: NormalizedRuleAction): NormalizedAlertAction | NormalizedSystemAction => ({
id,
params: params as AlertingRuleAction['params'],
...(alertsFilter && {
@ -70,6 +87,7 @@ export const transformNormalizedRuleToAlertAction = ({
alertsFilter: alertsFilter as AlertingRuleAction['alertsFilter'],
}),
...(frequency && { frequency }),
...(group && { group }),
});
export const transformAlertToNormalizedRuleAction = ({

View file

@ -4104,7 +4104,6 @@ components:
params:
$ref: '#/components/schemas/RuleActionParams'
required:
- group
- id
- params
NormalizedRuleError:
@ -4902,7 +4901,6 @@ components:
$ref: '#/components/schemas/NonEmptyString'
required:
- action_type_id
- group
- id
- params
RuleActionAlertsFilter:

View file

@ -3272,7 +3272,6 @@ components:
params:
$ref: '#/components/schemas/RuleActionParams'
required:
- group
- id
- params
NormalizedRuleError:
@ -4070,7 +4069,6 @@ components:
$ref: '#/components/schemas/NonEmptyString'
required:
- action_type_id
- group
- id
- params
RuleActionAlertsFilter:

View file

@ -9,10 +9,17 @@ import React from 'react';
import { EuiToolTip, EuiText, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
import type { ActionType, AsApiContract } from '@kbn/actions-plugin/common';
import type { ActionResult } from '@kbn/actions-plugin/server';
import type { RuleActionFrequency, RuleAction } from '@kbn/alerting-plugin/common';
import type {
RuleActionFrequency,
RuleAction,
RuleSystemAction,
} from '@kbn/alerting-plugin/common';
import type { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { getTimeTypeValue } from '../../../rule_creation_ui/pages/rule_creation/helpers';
import {
getTimeTypeValue,
isRuleAction as getIsRuleAction,
} from '../../../rule_creation_ui/pages/rule_creation/helpers';
import * as i18n from './translations';
const DescriptionLine = ({ children }: { children: React.ReactNode }) => (
@ -79,7 +86,7 @@ export const FrequencyDescription: React.FC<{ frequency?: RuleActionFrequency }>
};
interface NotificationActionProps {
action: RuleAction;
action: RuleAction | RuleSystemAction;
connectorTypes: ActionType[];
connectors: Array<AsApiContract<ActionResult>>;
actionTypeRegistry: ActionTypeRegistryContract;
@ -91,13 +98,23 @@ export function NotificationAction({
connectors,
actionTypeRegistry,
}: NotificationActionProps) {
const isRuleAction = getIsRuleAction(action, actionTypeRegistry);
const connectorType = connectorTypes.find(({ id }) => id === action.actionTypeId);
const connectorTypeName = connectorType?.name ?? '';
const registeredAction = actionTypeRegistry.get(action.actionTypeId);
/*
since there is no "connector" for system actions,
we need to determine the title based off the action
properties in order to render helpful text on the
rule details page.
*/
const connectorTypeName = isRuleAction
? connectorType?.name ?? ''
: registeredAction.actionTypeTitle ?? '';
const iconType = registeredAction?.iconClass ?? 'apps';
const connector = connectors.find(({ id }) => id === action.id);
const connectorName = connector?.name ?? '';
const iconType = actionTypeRegistry.get(action.actionTypeId)?.iconClass ?? 'apps';
const connectorName = (isRuleAction ? connector?.name : registeredAction.actionTypeTitle) ?? '';
return (
<EuiFlexItem>
@ -114,7 +131,13 @@ export function NotificationAction({
<EuiFlexItem grow={false}>
<EuiIcon size="s" type="bell" color="subdued" />
</EuiFlexItem>
<FrequencyDescription frequency={action.frequency} />
{isRuleAction ? (
<FrequencyDescription frequency={action.frequency} />
) : (
// Display frequency description for system action
// same text used by stack alerting
<DescriptionLine>{i18n.SYSTEM_ACTION_FREQUENCY}</DescriptionLine>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -86,3 +86,8 @@ export const PERIODICALLY = i18n.translate(
'xpack.securitySolution.detectionEngine.actionNotifyWhen.periodically',
{ defaultMessage: 'Periodically' }
);
export const SYSTEM_ACTION_FREQUENCY = i18n.translate(
'xpack.securitySolution.detectionEngine.actionNotifyWhen.systemActionFrequency',
{ defaultMessage: 'On check intervals' }
);

View file

@ -16,6 +16,7 @@ import type {
ScheduleStepRule,
TimeframePreviewOptions,
} from '../../../../detections/pages/detection_engine/rules/types';
import { useKibana } from '../../../../common/lib/kibana';
interface PreviewRouteParams {
defineRuleData?: DefineStepRule;
@ -34,6 +35,10 @@ export const usePreviewRoute = ({
}: PreviewRouteParams) => {
const [isRequestTriggered, setIsRequestTriggered] = useState(false);
const {
triggersActionsUi: { actionTypeRegistry },
} = useKibana().services;
const { isLoading, response, rule, setRule } = usePreviewRule({
timeframeOptions,
});
@ -72,6 +77,7 @@ export const usePreviewRoute = ({
defineRuleData,
aboutRuleData,
scheduleRuleData,
actionTypeRegistry,
exceptionsList,
})
);
@ -84,6 +90,7 @@ export const usePreviewRoute = ({
aboutRuleData,
scheduleRuleData,
exceptionsList,
actionTypeRegistry,
]);
return {

View file

@ -6,6 +6,9 @@
*/
import type { List } from '@kbn/securitysolution-io-ts-list-types';
import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock';
import type { ActionTypeRegistryContract } from '@kbn/alerts-ui-shared';
import type { RuleCreateProps } from '../../../../../common/api/detection_engine/model/rule_schema';
import type { Rule } from '../../../rule_management/logic';
import {
@ -1104,13 +1107,19 @@ describe('helpers', () => {
describe('formatActionsStepData', () => {
let mockData: ActionsStepRule;
const actionTypeRegistry = {
...actionTypeRegistryMock.create(),
get: jest.fn((actionTypeId: string) => ({
isSystemAction: false,
})),
} as unknown as jest.Mocked<ActionTypeRegistryContract>;
beforeEach(() => {
mockData = mockActionsStepRule();
});
test('returns formatted object as ActionsStepRuleJson', () => {
const result = formatActionsStepData(mockData);
const result = formatActionsStepData(mockData, actionTypeRegistry);
const expected: ActionsStepRuleJson = {
actions: [],
enabled: false,
@ -1134,7 +1143,7 @@ describe('helpers', () => {
...mockData,
actions: [mockAction],
};
const result = formatActionsStepData(mockStepData);
const result = formatActionsStepData(mockStepData, actionTypeRegistry);
const expected: ActionsStepRuleJson = {
actions: [
{
@ -1159,6 +1168,7 @@ describe('helpers', () => {
let mockDefine: DefineStepRule;
let mockSchedule: ScheduleStepRule;
let mockActions: ActionsStepRule;
const actionTypeRegistry = actionTypeRegistryMock.create();
beforeEach(() => {
mockAbout = mockAboutStepRule();
@ -1168,7 +1178,13 @@ describe('helpers', () => {
});
test('returns rule with type of query when saved_id exists but shouldLoadQueryDynamically=false', () => {
const result = formatRule<Rule>(mockDefine, mockAbout, mockSchedule, mockActions);
const result = formatRule<Rule>(
mockDefine,
mockAbout,
mockSchedule,
mockActions,
actionTypeRegistry
);
expect(result.type).toEqual('query');
});
@ -1178,7 +1194,8 @@ describe('helpers', () => {
{ ...mockDefine, shouldLoadQueryDynamically: true },
mockAbout,
mockSchedule,
mockActions
mockActions,
actionTypeRegistry
);
expect(result.type).toEqual('saved_query');
@ -1196,14 +1213,21 @@ describe('helpers', () => {
mockDefineStepRuleWithoutSavedId,
mockAbout,
mockSchedule,
mockActions
mockActions,
actionTypeRegistry
);
expect(result.type).toEqual('query');
});
test('returns rule without id if ruleId does not exist', () => {
const result = formatRule<RuleCreateProps>(mockDefine, mockAbout, mockSchedule, mockActions);
const result = formatRule<RuleCreateProps>(
mockDefine,
mockAbout,
mockSchedule,
mockActions,
actionTypeRegistry
);
expect(result).not.toHaveProperty<RuleCreateProps>('id');
});

View file

@ -25,10 +25,18 @@ import type {
Type,
} from '@kbn/securitysolution-io-ts-alerting-types';
import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants';
import type {
RuleAction as AlertingRuleAction,
RuleSystemAction as AlertingRuleSystemAction,
} from '@kbn/alerting-plugin/common';
import type { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
import { assertUnreachable } from '../../../../../common/utility_types';
import {
transformAlertToRuleAction,
transformAlertToRuleResponseAction,
transformAlertToRuleSystemAction,
} from '../../../../../common/detection_engine/transform_actions';
import type {
@ -637,11 +645,23 @@ export const formatAboutStepData = (
return resp;
};
export const formatActionsStepData = (actionsStepData: ActionsStepRule): ActionsStepRuleJson => {
export const isRuleAction = (
action: AlertingRuleAction | AlertingRuleSystemAction,
actionTypeRegistry: ActionTypeRegistryContract
): action is AlertingRuleAction => !actionTypeRegistry.get(action.actionTypeId).isSystemActionType;
export const formatActionsStepData = (
actionsStepData: ActionsStepRule,
actionTypeRegistry: ActionTypeRegistryContract
): ActionsStepRuleJson => {
const { actions = [], responseActions, enabled, kibanaSiemAppUrl } = actionsStepData;
return {
actions: actions.map((action) => transformAlertToRuleAction(action)),
actions: actions.map((action) =>
isRuleAction(action, actionTypeRegistry)
? transformAlertToRuleAction(action)
: transformAlertToRuleSystemAction(action)
),
response_actions: responseActions?.map(transformAlertToRuleResponseAction),
enabled,
meta: {
@ -658,13 +678,14 @@ export const formatRule = <T>(
aboutStepData: AboutStepRule,
scheduleData: ScheduleStepRule,
actionsData: ActionsStepRule,
actionTypeRegistry: ActionTypeRegistryContract,
exceptionsList?: List[]
): T =>
deepmerge.all([
formatDefineStepData(defineStepData),
formatAboutStepData(aboutStepData, exceptionsList),
formatScheduleStepData(scheduleData),
formatActionsStepData(actionsData),
formatActionsStepData(actionsData, actionTypeRegistry),
]) as unknown as T;
export const formatPreviewRule = ({
@ -672,10 +693,12 @@ export const formatPreviewRule = ({
aboutRuleData,
scheduleRuleData,
exceptionsList,
actionTypeRegistry,
}: {
defineRuleData: DefineStepRule;
aboutRuleData: AboutStepRule;
scheduleRuleData: ScheduleStepRule;
actionTypeRegistry: ActionTypeRegistryContract;
exceptionsList?: List[];
}): RuleCreateProps => {
const aboutStepData = {
@ -689,6 +712,7 @@ export const formatPreviewRule = ({
aboutStepData,
scheduleRuleData,
stepActionsDefaultValue,
actionTypeRegistry,
exceptionsList
),
};

View file

@ -125,6 +125,7 @@ const CreateRulePageComponent: React.FC = () => {
const {
application,
data: { dataViews },
triggersActionsUi,
} = useKibana().services;
const loading = userInfoLoading || listsConfigLoading;
const [activeStep, setActiveStep] = useState<RuleStep>(RuleStep.defineRule);
@ -379,7 +380,8 @@ const CreateRulePageComponent: React.FC = () => {
{
...localActionsStepData,
enabled,
}
},
triggersActionsUi.actionTypeRegistry
)
),
]);
@ -405,6 +407,7 @@ const CreateRulePageComponent: React.FC = () => {
ruleType,
startMlJobs,
defineFieldsTransform,
triggersActionsUi.actionTypeRegistry,
]
);

View file

@ -84,7 +84,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
] = useUserData();
const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration } =
useListsConfig();
const { data: dataServices, application } = useKibana().services;
const { data: dataServices, application, triggersActionsUi } = useKibana().services;
const { navigateToApp } = application;
const { detailName: ruleId } = useParams<{ detailName: string }>();
@ -405,6 +405,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
aboutStepData,
scheduleStepData,
actionsStepData,
triggersActionsUi.actionTypeRegistry,
rule?.exceptions_list
),
...(ruleId ? { id: ruleId } : {}),
@ -431,6 +432,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
ruleId,
dispatchToaster,
navigateToApp,
triggersActionsUi.actionTypeRegistry,
]);
const onTabClick = useCallback(async (tab: EuiTabbedContentTab) => {

View file

@ -17,7 +17,10 @@ import type {
Type,
} from '@kbn/securitysolution-io-ts-alerting-types';
import type { DataViewBase, Filter } from '@kbn/es-query';
import type { RuleAction as AlertingRuleAction } from '@kbn/alerting-plugin/common';
import type {
RuleAction as AlertingRuleAction,
RuleSystemAction as AlertingRuleSystemAction,
} from '@kbn/alerting-plugin/common';
import type { DataViewListItem } from '@kbn/data-views-plugin/common';
import type { FieldValueQueryBar } from '../../../../detection_engine/rule_creation_ui/components/query_bar';
@ -190,7 +193,7 @@ export interface ScheduleStepRule {
}
export interface ActionsStepRule {
actions: AlertingRuleAction[];
actions: Array<AlertingRuleAction | AlertingRuleSystemAction>;
responseActions?: RuleResponseAction[];
enabled: boolean;
kibanaSiemAppUrl?: string;

View file

@ -141,6 +141,7 @@ export const performBulkActionRoute = (
// rulesClient method, hence there is no need to use fetchRulesByQueryOrIds utility
if (body.action === BulkActionTypeEnum.edit && !isDryRun) {
const { rules, errors, skipped } = await bulkEditRules({
actionsClient,
rulesClient,
filter: query,
ids: body.ids,

View file

@ -118,7 +118,8 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C
const migratedParsedObjectsWithoutDuplicateErrors = await migrateLegacyActionsIds(
parsedObjectsWithoutDuplicateErrors,
actionSOClient
actionSOClient,
actionsClient
);
// import actions-connectors
@ -158,6 +159,7 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C
existingLists: foundReferencedExceptionLists,
allowMissingConnectorSecrets: !!actionConnectors.length,
});
const errorsResp = importRuleResponse.filter((resp) => isBulkError(resp)) as BulkError[];
const successes = importRuleResponse.filter((resp) => {
if (isImportRegular(resp)) {

View file

@ -7,6 +7,7 @@
import { v4 as uuidv4 } from 'uuid';
import type { SanitizedRule } from '@kbn/alerting-plugin/common';
import type { RuleParams } from '../../../rule_schema';
import { duplicateRule } from './duplicate_rule';
@ -113,6 +114,7 @@ describe('duplicateRule', () => {
consumer: rule.consumer,
schedule: rule.schedule,
actions: rule.actions,
systemActions: rule.actions,
enabled: false, // covered in a separate test
});
});

View file

@ -9,6 +9,7 @@ import { v4 as uuidv4 } from 'uuid';
import { i18n } from '@kbn/i18n';
import { ruleTypeMappings } from '@kbn/securitysolution-rules';
import type { SanitizedRule } from '@kbn/alerting-plugin/common';
import { SERVER_APP_ID } from '../../../../../../common/constants';
import type { InternalRuleCreate, RuleParams } from '../../../rule_schema';
import { transformToActionFrequency } from '../../normalization/rule_actions';
@ -33,6 +34,7 @@ export const duplicateRule = async ({ rule }: DuplicateRuleParams): Promise<Inte
const isPrebuilt = rule.params.immutable;
const relatedIntegrations = isPrebuilt ? [] : rule.params.relatedIntegrations;
const requiredFields = isPrebuilt ? [] : rule.params.requiredFields;
const actions = transformToActionFrequency(rule.actions, rule.throttle);
// Duplicated rules are always considered custom rules
@ -57,5 +59,6 @@ export const duplicateRule = async ({ rule }: DuplicateRuleParams): Promise<Inte
schedule: rule.schedule,
enabled: false,
actions,
systemActions: rule.systemActions ?? [],
};
};

View file

@ -5,13 +5,17 @@
* 2.0.
*/
import type { ActionsClient } from '@kbn/actions-plugin/server';
import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management';
import { bulkEditActionToRulesClientOperation } from './action_to_rules_client_operation';
describe('bulkEditActionToRulesClientOperation', () => {
const actionsClient = {
isSystemAction: jest.fn((id: string) => id === 'system-connector-.cases'),
} as unknown as jest.Mocked<ActionsClient>;
test('should transform tags bulk edit actions correctly', () => {
expect(
bulkEditActionToRulesClientOperation({
bulkEditActionToRulesClientOperation(actionsClient, {
type: BulkActionEditTypeEnum.add_tags,
value: ['test'],
})
@ -25,7 +29,10 @@ describe('bulkEditActionToRulesClientOperation', () => {
});
expect(
bulkEditActionToRulesClientOperation({ type: BulkActionEditTypeEnum.set_tags, value: ['test'] })
bulkEditActionToRulesClientOperation(actionsClient, {
type: BulkActionEditTypeEnum.set_tags,
value: ['test'],
})
).toEqual([
{
field: 'tags',
@ -35,7 +42,7 @@ describe('bulkEditActionToRulesClientOperation', () => {
]);
expect(
bulkEditActionToRulesClientOperation({
bulkEditActionToRulesClientOperation(actionsClient, {
type: BulkActionEditTypeEnum.delete_tags,
value: ['test'],
})
@ -49,7 +56,7 @@ describe('bulkEditActionToRulesClientOperation', () => {
test('should transform schedule bulk edit correctly', () => {
expect(
bulkEditActionToRulesClientOperation({
bulkEditActionToRulesClientOperation(actionsClient, {
type: BulkActionEditTypeEnum.set_schedule,
value: {
interval: '100m',
@ -64,4 +71,157 @@ describe('bulkEditActionToRulesClientOperation', () => {
},
]);
});
test('should add_rule_actions non-system actions', () => {
expect(
bulkEditActionToRulesClientOperation(actionsClient, {
type: BulkActionEditTypeEnum.add_rule_actions,
value: {
actions: [
{
group: 'default',
id: 'b0d183b2-3e04-428d-9fc4-ca7e23604380',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
},
],
},
})
).toEqual([
{
field: 'actions',
operation: 'add',
value: [
{
group: 'default',
id: 'b0d183b2-3e04-428d-9fc4-ca7e23604380',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
},
],
},
]);
});
test('should add_rule_actions system actions', () => {
expect(
bulkEditActionToRulesClientOperation(actionsClient, {
type: BulkActionEditTypeEnum.add_rule_actions,
value: {
actions: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.name'],
},
},
},
],
},
})
).toEqual([
{
field: 'actions',
operation: 'add',
value: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.name'],
},
},
},
],
},
]);
});
test('should set_rule_actions non-system actions', () => {
expect(
bulkEditActionToRulesClientOperation(actionsClient, {
type: BulkActionEditTypeEnum.set_rule_actions,
value: {
actions: [
{
group: 'default',
id: 'b0d183b2-3e04-428d-9fc4-ca7e23604380',
params: {
message:
'How many alerts were generated? This many alerts: {{state.signals_count}}',
},
frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
},
],
},
})
).toEqual([
{
field: 'actions',
operation: 'set',
value: [
{
id: 'b0d183b2-3e04-428d-9fc4-ca7e23604380',
params: {
message: 'How many alerts were generated? This many alerts: {{state.signals_count}}',
},
frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
group: 'default',
},
],
},
]);
});
test('should set_rule_actions system actions', () => {
expect(
bulkEditActionToRulesClientOperation(actionsClient, {
type: BulkActionEditTypeEnum.set_rule_actions,
value: {
actions: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.type'],
},
},
},
],
},
})
).toEqual([
{
field: 'actions',
operation: 'set',
value: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.type'],
},
},
},
],
},
]);
});
});

View file

@ -6,12 +6,12 @@
*/
import type { BulkEditOperation } from '@kbn/alerting-plugin/server';
import { transformNormalizedRuleToAlertAction } from '../../../../../../common/detection_engine/transform_actions';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type { BulkActionEditForRuleAttributes } from '../../../../../../common/api/detection_engine/rule_management';
import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management';
import { assertUnreachable } from '../../../../../../common/utility_types';
import { transformToActionFrequency } from '../../normalization/rule_actions';
import { parseAndTransformRuleActions } from './utils';
/**
* converts bulk edit action to format of rulesClient.bulkEdit operation
@ -19,6 +19,7 @@ import { transformToActionFrequency } from '../../normalization/rule_actions';
* @returns rulesClient BulkEditOperation
*/
export const bulkEditActionToRulesClientOperation = (
actionsClient: ActionsClient,
action: BulkActionEditForRuleAttributes
): BulkEditOperation[] => {
switch (action.type) {
@ -56,8 +57,10 @@ export const bulkEditActionToRulesClientOperation = (
{
field: 'actions',
operation: 'add',
value: transformToActionFrequency(action.value.actions, action.value.throttle).map(
transformNormalizedRuleToAlertAction
value: parseAndTransformRuleActions(
actionsClient,
action.value.actions,
action.value.throttle
),
},
];
@ -67,8 +70,10 @@ export const bulkEditActionToRulesClientOperation = (
{
field: 'actions',
operation: 'set',
value: transformToActionFrequency(action.value.actions, action.value.throttle).map(
transformNormalizedRuleToAlertAction
value: parseAndTransformRuleActions(
actionsClient,
action.value.actions,
action.value.throttle
),
},
];

View file

@ -6,6 +6,7 @@
*/
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type { ExperimentalFeatures } from '../../../../../../common';
import type { BulkActionEditPayload } from '../../../../../../common/api/detection_engine/rule_management';
@ -21,6 +22,7 @@ import { validateBulkEditRule } from './validations';
import { bulkEditActionToRulesClientOperation } from './action_to_rules_client_operation';
export interface BulkEditRulesArguments {
actionsClient: ActionsClient;
rulesClient: RulesClient;
actions: BulkActionEditPayload[];
filter?: string;
@ -37,6 +39,7 @@ export interface BulkEditRulesArguments {
* @returns edited rules and caught errors
*/
export const bulkEditRules = async ({
actionsClient,
rulesClient,
ids,
actions,
@ -45,7 +48,9 @@ export const bulkEditRules = async ({
experimentalFeatures,
}: BulkEditRulesArguments) => {
const { attributesActions, paramsActions } = splitBulkEditActions(actions);
const operations = attributesActions.map(bulkEditActionToRulesClientOperation).flat();
const operations = attributesActions
.map((attribute) => bulkEditActionToRulesClientOperation(actionsClient, attribute))
.flat();
const result = await rulesClient.bulkEdit({
...(ids ? { ids } : { filter: enrichFilterWithRuleTypeMapping(filter) }),
operations,

View file

@ -4,8 +4,17 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { BulkActionEditType } from '../../../../../../common/api/detection_engine/rule_management';
import { isEmpty, partition } from 'lodash';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type {
BulkActionEditType,
NormalizedRuleAction,
ThrottleForBulkActions,
} from '../../../../../../common/api/detection_engine/rule_management';
import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management';
import { transformToActionFrequency } from '../../normalization/rule_actions';
import { transformNormalizedRuleToAlertAction } from '../../../../../../common/detection_engine/transform_actions';
/**
* helper utility that defines whether bulk edit action is related to index patterns, i.e. one of:
@ -21,3 +30,27 @@ export const isIndexPatternsBulkEditAction = (editAction: BulkActionEditType) =>
];
return indexPatternsActions.includes(editAction);
};
/**
* Separates system actions from actions and performs necessary transformations for
* alerting rules client bulk edit operations.
* @param actionsClient
* @param actions
* @param throttle
* @returns
*/
export const parseAndTransformRuleActions = (
actionsClient: ActionsClient,
actions: NormalizedRuleAction[],
throttle: ThrottleForBulkActions | undefined
) => {
const [systemActions, extActions] = !isEmpty(actions)
? partition(actions, (action: NormalizedRuleAction) => actionsClient.isSystemAction(action.id))
: [[], actions];
return [
...(systemActions ?? []),
...transformToActionFrequency(extActions ?? [], throttle).map(
transformNormalizedRuleToAlertAction
),
];
};

View file

@ -6,6 +6,9 @@
*/
import type { UpdateRuleData } from '@kbn/alerting-plugin/server/application/rule/methods/update';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type { RuleActionCamel } from '@kbn/securitysolution-io-ts-alerting-types';
import type {
RuleResponse,
TypeSpecificCreateProps,
@ -22,7 +25,7 @@ import { assertUnreachable } from '../../../../../../../common/utility_types';
import { convertObjectKeysToCamelCase } from '../../../../../../utils/object_case_converters';
import type { RuleParams, TypeSpecificRuleParams } from '../../../../rule_schema';
import { transformToActionFrequency } from '../../../normalization/rule_actions';
import { addEcsToRequiredFields } from '../../../utils/utils';
import { addEcsToRequiredFields, separateActionsAndSystemAction } from '../../../utils/utils';
/**
* These are the fields that are added to the rule response that are not part of the rule params
@ -37,10 +40,17 @@ type RuntimeFields =
| 'execution_summary';
export const convertRuleResponseToAlertingRule = (
rule: Omit<RuleResponse, RuntimeFields>
rule: Omit<RuleResponse, RuntimeFields>,
actionsClient: ActionsClient
): UpdateRuleData<RuleParams> => {
const alertActions = rule.actions.map((action) => transformRuleToAlertAction(action));
const actions = transformToActionFrequency(alertActions, rule.throttle);
const [ruleSystemActions, ruleActions] = separateActionsAndSystemAction(
actionsClient,
rule.actions
);
const systemActions = ruleSystemActions?.map((action) => transformRuleToAlertAction(action));
const alertActions = ruleActions?.map((action) => transformRuleToAlertAction(action)) ?? [];
const actions = transformToActionFrequency(alertActions as RuleActionCamel[], rule.throttle);
// Because of Omit<RuleResponse, RuntimeFields> Typescript doesn't recognize
// that rule is assignable to TypeSpecificCreateProps despite omitted fields
@ -87,6 +97,7 @@ export const convertRuleResponseToAlertingRule = (
},
schedule: { interval: rule.interval },
actions,
...(systemActions && { systemActions }),
};
};

View file

@ -8,7 +8,10 @@
import type { ResolvedSanitizedRule, SanitizedRule } from '@kbn/alerting-plugin/common';
import type { RequiredOptional } from '@kbn/zod-helpers';
import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
import { transformAlertToRuleAction } from '../../../../../../../common/detection_engine/transform_actions';
import {
transformAlertToRuleAction,
transformAlertToRuleSystemAction,
} from '../../../../../../../common/detection_engine/transform_actions';
import { createRuleExecutionSummary } from '../../../../rule_monitoring';
import type { RuleParams } from '../../../../rule_schema';
import {
@ -32,6 +35,10 @@ export const internalRuleToAPIResponse = (
const alertActions = rule.actions.map(transformAlertToRuleAction);
const throttle = transformFromAlertThrottle(rule);
const actions = transformToActionFrequency(alertActions, throttle);
const systemActions = rule.systemActions?.map((action) => {
const transformedAction = transformAlertToRuleSystemAction(action);
return transformedAction;
});
const normalizedRuleParams = normalizeRuleParams(rule.params);
return {
@ -56,7 +63,7 @@ export const internalRuleToAPIResponse = (
...typeSpecificCamelToSnake(rule.params),
// Actions
throttle: undefined,
actions,
actions: [...actions, ...(systemActions ?? [])],
// Execution summary
execution_summary: executionSummary ?? undefined,
};

View file

@ -6,6 +6,7 @@
*/
import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import {
@ -29,15 +30,28 @@ describe('DetectionRulesClient.createCustomRule', () => {
let detectionRulesClient: IDetectionRulesClient;
const mlAuthz = (buildMlAuthz as jest.Mock)();
let actionsClient = {
isSystemAction: jest.fn((id: string) => id === 'system-connector-.cases'),
} as unknown as jest.Mocked<ActionsClient>;
beforeEach(() => {
jest.resetAllMocks();
actionsClient = {
isSystemAction: jest.fn((id: string) => id === 'system-connector-.cases'),
} as unknown as jest.Mocked<ActionsClient>;
rulesClient = rulesClientMock.create();
// creates a rule with a system action and a connector action
rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams()));
const savedObjectsClient = savedObjectsClientMock.create();
detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient });
detectionRulesClient = createDetectionRulesClient({
actionsClient,
rulesClient,
mlAuthz,
savedObjectsClient,
});
});
it('should create a rule with the correct parameters and options', async () => {
@ -58,6 +72,256 @@ describe('DetectionRulesClient.createCustomRule', () => {
);
});
it('should create a rule with actions and system actions', async () => {
rulesClient.create.mockResolvedValue(
getRuleMock(getQueryRuleParams(), {
actions: [
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
actionTypeId: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
group: 'default',
},
],
systemActions: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.name'],
},
},
actionTypeId: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
],
})
);
const params = {
...getCreateRulesSchemaMock(),
actions: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.name'],
},
},
action_type_id: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
action_type_id: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: {
summary: true,
notifyWhen: 'onActiveAlert' as 'onActiveAlert', // needed for type check on line 127
throttle: null,
},
group: 'default',
},
],
};
await detectionRulesClient.createCustomRule({
params,
});
expect(rulesClient.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
actions: expect.arrayContaining([
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
actionTypeId: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: {
summary: true,
notifyWhen: 'onActiveAlert' as 'onActiveAlert', // needed for type check on line 127
throttle: null,
},
group: 'default',
},
]),
systemActions: expect.arrayContaining([
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.name'],
},
},
actionTypeId: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
]),
enabled: true,
params: expect.objectContaining({
description: params.description,
immutable: false,
}),
}),
})
);
});
it('should create a rule with system actions', async () => {
rulesClient.create.mockResolvedValue(
getRuleMock(getQueryRuleParams(), {
systemActions: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.name'],
},
},
actionTypeId: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
],
})
);
const params = {
...getCreateRulesSchemaMock(),
actions: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.name'],
},
},
action_type_id: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
],
};
await detectionRulesClient.createCustomRule({ params });
expect(rulesClient.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
systemActions: expect.arrayContaining([
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.name'],
},
},
actionTypeId: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
]),
enabled: true,
params: expect.objectContaining({
description: params.description,
immutable: false,
}),
}),
})
);
});
it('should create a rule with actions', async () => {
rulesClient.create.mockResolvedValue(
getRuleMock(getQueryRuleParams(), {
actions: [
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
actionTypeId: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
group: 'default',
},
],
})
);
const params = {
...getCreateRulesSchemaMock(),
actions: [
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
action_type_id: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: {
summary: true,
notifyWhen: 'onActiveAlert' as 'onActiveAlert', // needed for type check on line 127
throttle: null,
},
group: 'default',
},
],
};
await detectionRulesClient.createCustomRule({ params });
expect(rulesClient.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
actions: expect.arrayContaining([
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
actionTypeId: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: {
summary: true,
notifyWhen: 'onActiveAlert' as 'onActiveAlert', // needed for type check on line 127
throttle: null,
},
group: 'default',
},
]),
enabled: true,
params: expect.objectContaining({
description: params.description,
immutable: false,
}),
}),
})
);
});
it('throws if mlAuth fails', async () => {
(throwAuthzError as jest.Mock).mockImplementationOnce(() => {
throw new Error('mocked MLAuth error');

View file

@ -6,6 +6,7 @@
*/
import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import {
@ -29,6 +30,7 @@ describe('DetectionRulesClient.createPrebuiltRule', () => {
let detectionRulesClient: IDetectionRulesClient;
const mlAuthz = (buildMlAuthz as jest.Mock)();
let actionsClient: jest.Mocked<ActionsClient>;
beforeEach(() => {
jest.resetAllMocks();
@ -37,7 +39,12 @@ describe('DetectionRulesClient.createPrebuiltRule', () => {
rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams()));
const savedObjectsClient = savedObjectsClientMock.create();
detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient });
detectionRulesClient = createDetectionRulesClient({
actionsClient,
rulesClient,
mlAuthz,
savedObjectsClient,
});
});
it('creates a rule with the correct parameters and options', async () => {

View file

@ -6,6 +6,8 @@
*/
import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import { buildMlAuthz } from '../../../../machine_learning/authz';
import { createDetectionRulesClient } from './detection_rules_client';
@ -18,11 +20,17 @@ describe('DetectionRulesClient.deleteRule', () => {
let detectionRulesClient: IDetectionRulesClient;
const mlAuthz = (buildMlAuthz as jest.Mock)();
let actionsClient: jest.Mocked<ActionsClient>;
beforeEach(() => {
rulesClient = rulesClientMock.create();
const savedObjectsClient = savedObjectsClientMock.create();
detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient });
detectionRulesClient = createDetectionRulesClient({
actionsClient,
rulesClient,
mlAuthz,
savedObjectsClient,
});
});
it('should call rulesClient.delete passing the expected ruleId', async () => {

View file

@ -6,6 +6,8 @@
*/
import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import {
getCreateRulesSchemaMock,
@ -29,6 +31,8 @@ describe('DetectionRulesClient.importRule', () => {
let detectionRulesClient: IDetectionRulesClient;
const mlAuthz = (buildMlAuthz as jest.Mock)();
let actionsClient: jest.Mocked<ActionsClient>;
const immutable = false as const; // Can only take value of false
const allowMissingConnectorSecrets = true;
const ruleToImport = {
@ -46,7 +50,12 @@ describe('DetectionRulesClient.importRule', () => {
rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams()));
rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams()));
const savedObjectsClient = savedObjectsClientMock.create();
detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient });
detectionRulesClient = createDetectionRulesClient({
actionsClient,
rulesClient,
mlAuthz,
savedObjectsClient,
});
});
it('calls rulesClient.create with the correct parameters when rule_id does not match an installed rule', async () => {

View file

@ -6,6 +6,7 @@
*/
import { rulesClientMock } from '@kbn/alerting-plugin/server/rules_client.mock';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import { getRuleMock } from '../../../routes/__mocks__/request_responses';
import { getMlRuleParams, getQueryRuleParams } from '../../../rule_schema/mocks';
@ -33,10 +34,23 @@ describe('DetectionRulesClient.patchRule', () => {
const mlAuthz = (buildMlAuthz as jest.Mock)();
let actionsClient = {
isSystemAction: jest.fn((id: string) => id === 'system-connector-.cases'),
} as unknown as jest.Mocked<ActionsClient>;
beforeEach(() => {
actionsClient = {
isSystemAction: jest.fn((id: string) => id === 'system-connector-.cases'),
} as unknown as jest.Mocked<ActionsClient>;
rulesClient = rulesClientMock.create();
const savedObjectsClient = savedObjectsClientMock.create();
detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient });
detectionRulesClient = createDetectionRulesClient({
actionsClient,
rulesClient,
mlAuthz,
savedObjectsClient,
});
});
it('calls the rulesClient with expected params', async () => {
@ -68,6 +82,114 @@ describe('DetectionRulesClient.patchRule', () => {
);
});
it('calls rule update with rule system actions if nextParams has system actions', async () => {
const rulePatch = {
...getCreateRulesSchemaMock(),
actions: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.type'], // changing this value
},
},
action_type_id: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
action_type_id: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: {
summary: true,
notifyWhen: 'onActiveAlert' as 'onActiveAlert', // needed for type check on line 127
throttle: null,
},
group: 'default',
},
],
};
const existingRule = getRulesSchemaMock();
(getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule);
rulesClient.update.mockResolvedValue(
getRuleMock(getQueryRuleParams(), {
actions: [
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
actionTypeId: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
group: 'default',
},
],
systemActions: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.type'],
},
},
actionTypeId: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
],
})
);
await detectionRulesClient.patchRule({ rulePatch });
expect(rulesClient.update).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
actions: expect.arrayContaining([
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
actionTypeId: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: {
summary: true,
notifyWhen: 'onActiveAlert' as 'onActiveAlert', // needed for type check on line 127
throttle: null,
},
group: 'default',
},
]),
systemActions: expect.arrayContaining([
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.type'],
},
},
actionTypeId: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
]),
}),
})
);
});
it('enables the rule if the nexParams have enabled: true', async () => {
// Mock the existing rule
const existingRule = getRulesSchemaMock();

View file

@ -6,7 +6,9 @@
*/
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type { SavedObjectsClientContract } from '@kbn/core/server';
import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema';
import { withSecuritySpan } from '../../../../../utils/with_security_span';
import type { MlAuthz } from '../../../../machine_learning/authz';
@ -29,12 +31,14 @@ import { updateRule } from './methods/update_rule';
import { upgradePrebuiltRule } from './methods/upgrade_prebuilt_rule';
interface DetectionRulesClientParams {
actionsClient: ActionsClient;
rulesClient: RulesClient;
savedObjectsClient: SavedObjectsClientContract;
mlAuthz: MlAuthz;
}
export const createDetectionRulesClient = ({
actionsClient,
rulesClient,
mlAuthz,
savedObjectsClient,
@ -45,6 +49,7 @@ export const createDetectionRulesClient = ({
async createCustomRule(args: CreateCustomRuleArgs): Promise<RuleResponse> {
return withSecuritySpan('DetectionRulesClient.createCustomRule', async () => {
return createRule({
actionsClient,
rulesClient,
rule: {
...args.params,
@ -62,6 +67,7 @@ export const createDetectionRulesClient = ({
async createPrebuiltRule(args: CreatePrebuiltRuleArgs): Promise<RuleResponse> {
return withSecuritySpan('DetectionRulesClient.createPrebuiltRule', async () => {
return createRule({
actionsClient,
rulesClient,
rule: {
...args.params,
@ -74,13 +80,25 @@ export const createDetectionRulesClient = ({
async updateRule({ ruleUpdate }: UpdateRuleArgs): Promise<RuleResponse> {
return withSecuritySpan('DetectionRulesClient.updateRule', async () => {
return updateRule({ rulesClient, prebuiltRuleAssetClient, mlAuthz, ruleUpdate });
return updateRule({
actionsClient,
rulesClient,
prebuiltRuleAssetClient,
mlAuthz,
ruleUpdate,
});
});
},
async patchRule({ rulePatch }: PatchRuleArgs): Promise<RuleResponse> {
return withSecuritySpan('DetectionRulesClient.patchRule', async () => {
return patchRule({ rulesClient, prebuiltRuleAssetClient, mlAuthz, rulePatch });
return patchRule({
actionsClient,
rulesClient,
prebuiltRuleAssetClient,
mlAuthz,
rulePatch,
});
});
},
@ -93,6 +111,7 @@ export const createDetectionRulesClient = ({
async upgradePrebuiltRule({ ruleAsset }: UpgradePrebuiltRuleArgs): Promise<RuleResponse> {
return withSecuritySpan('DetectionRulesClient.upgradePrebuiltRule', async () => {
return upgradePrebuiltRule({
actionsClient,
rulesClient,
ruleAsset,
mlAuthz,
@ -104,6 +123,7 @@ export const createDetectionRulesClient = ({
async importRule(args: ImportRuleArgs): Promise<RuleResponse> {
return withSecuritySpan('DetectionRulesClient.importRule', async () => {
return importRule({
actionsClient,
rulesClient,
importRulePayload: args,
mlAuthz,

View file

@ -6,6 +6,7 @@
*/
import { rulesClientMock } from '@kbn/alerting-plugin/server/rules_client.mock';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import { getRuleMock } from '../../../routes/__mocks__/request_responses';
import { getMlRuleParams, getQueryRuleParams } from '../../../rule_schema/mocks';
@ -32,11 +33,24 @@ describe('DetectionRulesClient.updateRule', () => {
let detectionRulesClient: IDetectionRulesClient;
const mlAuthz = (buildMlAuthz as jest.Mock)();
let actionsClient = {
isSystemAction: jest.fn((id: string) => id === 'system-connector-.cases'),
} as unknown as jest.Mocked<ActionsClient>;
beforeEach(() => {
actionsClient = {
isSystemAction: jest.fn((id: string) => id === 'system-connector-.cases'),
} as unknown as jest.Mocked<ActionsClient>;
rulesClient = rulesClientMock.create();
const savedObjectsClient = savedObjectsClientMock.create();
detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient });
detectionRulesClient = createDetectionRulesClient({
actionsClient,
rulesClient,
mlAuthz,
savedObjectsClient,
});
});
it('calls the rulesClient with expected params', async () => {
@ -68,6 +82,254 @@ describe('DetectionRulesClient.updateRule', () => {
);
});
it('calls the rulesClient with actions and system actions', async () => {
const ruleUpdate = {
...getCreateRulesSchemaMock(),
actions: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.name'],
},
},
action_type_id: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
action_type_id: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: {
summary: true,
notifyWhen: 'onActiveAlert' as 'onActiveAlert', // needed for type check on line 127
throttle: null,
},
group: 'default',
},
],
};
const existingRule = getRulesSchemaMock();
(getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule);
rulesClient.update.mockResolvedValue(
getRuleMock(getQueryRuleParams(), {
actions: [
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
actionTypeId: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
group: 'default',
},
],
systemActions: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.name'],
},
},
actionTypeId: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
],
})
);
await detectionRulesClient.updateRule({ ruleUpdate });
expect(rulesClient.update).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
actions: expect.arrayContaining([
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
actionTypeId: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: {
summary: true,
notifyWhen: 'onActiveAlert' as 'onActiveAlert', // needed for type check on line 127
throttle: null,
},
group: 'default',
},
]),
systemActions: expect.arrayContaining([
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.name'],
},
},
actionTypeId: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
]),
}),
})
);
});
it('calls the rulesClient when updating a system action groupingBy property from agent.name to agent.type', async () => {
const ruleUpdate = {
...getCreateRulesSchemaMock(),
actions: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.type'], // changing this value
},
},
action_type_id: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
action_type_id: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: {
summary: true,
notifyWhen: 'onActiveAlert' as 'onActiveAlert', // needed for type check on line 127
throttle: null,
},
group: 'default',
},
],
};
const existingRule = getRuleMock(getQueryRuleParams(), {
systemActions: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.name'], // changing this value
},
},
actionTypeId: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
],
actions: [
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
actionTypeId: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: {
summary: true,
notifyWhen: 'onActiveAlert' as 'onActiveAlert', // needed for type check on line 127
throttle: null,
},
group: 'default',
},
],
});
(getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule);
rulesClient.update.mockResolvedValue(
getRuleMock(getQueryRuleParams(), {
actions: [
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
actionTypeId: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
group: 'default',
},
],
systemActions: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.type'],
},
},
actionTypeId: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
],
})
);
await detectionRulesClient.updateRule({ ruleUpdate });
expect(rulesClient.update).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
actions: expect.arrayContaining([
{
id: 'b7da98d0-e1ef-4954-969f-e69c9ef5f65d',
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
actionTypeId: '.slack',
uuid: '4c3601b5-74b9-4330-b2f3-fea4ea3dc046',
frequency: {
summary: true,
notifyWhen: 'onActiveAlert' as 'onActiveAlert', // needed for type check on line 127
throttle: null,
},
group: 'default',
},
]),
systemActions: expect.arrayContaining([
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.type'],
},
},
actionTypeId: '.cases',
uuid: 'e62cbe00-a0e1-44d9-9585-c39e5da63d6f',
},
]),
}),
})
);
});
it('calls the rulesClient with new ML params', async () => {
// Mock the existing rule
const existingRule = getRulesMlSchemaMock();

View file

@ -6,6 +6,7 @@
*/
import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import {
getCreateEqlRuleSchemaMock,
@ -32,11 +33,22 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => {
let detectionRulesClient: IDetectionRulesClient;
const mlAuthz = (buildMlAuthz as jest.Mock)();
let actionsClient = {
isSystemAction: jest.fn((id: string) => id === 'system-connector-.cases'),
} as unknown as jest.Mocked<ActionsClient>;
beforeEach(() => {
actionsClient = {
isSystemAction: jest.fn((id: string) => id === 'system-connector-.cases'),
} as unknown as jest.Mocked<ActionsClient>;
rulesClient = rulesClientMock.create();
const savedObjectsClient = savedObjectsClientMock.create();
detectionRulesClient = createDetectionRulesClient({ rulesClient, mlAuthz, savedObjectsClient });
detectionRulesClient = createDetectionRulesClient({
actionsClient,
rulesClient,
mlAuthz,
savedObjectsClient,
});
});
it('throws if no matching rule_id is found', async () => {

View file

@ -6,6 +6,7 @@
*/
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import { ruleTypeMappings } from '@kbn/securitysolution-rules';
import { SERVER_APP_ID } from '../../../../../../../common';
import type {
@ -20,6 +21,7 @@ import { applyRuleDefaults } from '../mergers/apply_rule_defaults';
import { validateMlAuth } from '../utils';
interface CreateRuleOptions {
actionsClient: ActionsClient;
rulesClient: RulesClient;
mlAuthz: MlAuthz;
rule: RuleCreateProps & { immutable: boolean };
@ -28,6 +30,7 @@ interface CreateRuleOptions {
}
export const createRule = async ({
actionsClient,
rulesClient,
mlAuthz,
rule,
@ -39,7 +42,7 @@ export const createRule = async ({
const ruleWithDefaults = applyRuleDefaults(rule);
const payload = {
...convertRuleResponseToAlertingRule(ruleWithDefaults),
...convertRuleResponseToAlertingRule(ruleWithDefaults, actionsClient),
alertTypeId: ruleTypeMappings[rule.type],
consumer: SERVER_APP_ID,
enabled: rule.enabled ?? false,

View file

@ -6,6 +6,8 @@
*/
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
import type { MlAuthz } from '../../../../../machine_learning/authz';
import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client';
@ -19,6 +21,7 @@ import { createRule } from './create_rule';
import { getRuleByRuleId } from './get_rule_by_rule_id';
interface ImportRuleOptions {
actionsClient: ActionsClient;
rulesClient: RulesClient;
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
importRulePayload: ImportRuleArgs;
@ -26,6 +29,7 @@ interface ImportRuleOptions {
}
export const importRule = async ({
actionsClient,
rulesClient,
importRulePayload,
prebuiltRuleAssetClient,
@ -57,13 +61,14 @@ export const importRule = async ({
const updatedRule = await rulesClient.update({
id: existingRule.id,
data: convertRuleResponseToAlertingRule(ruleWithUpdates),
data: convertRuleResponseToAlertingRule(ruleWithUpdates, actionsClient),
});
return convertAlertingRuleToRuleResponse(updatedRule);
}
/* Rule does not exist, so we'll create it */
return createRule({
actionsClient,
rulesClient,
mlAuthz,
rule: ruleToImport,

View file

@ -6,6 +6,8 @@
*/
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type {
RulePatchProps,
RuleResponse,
@ -20,6 +22,7 @@ import { ClientError, toggleRuleEnabledOnUpdate, validateMlAuth } from '../utils
import { getRuleByIdOrRuleId } from './get_rule_by_id_or_rule_id';
interface PatchRuleOptions {
actionsClient: ActionsClient;
rulesClient: RulesClient;
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
rulePatch: RulePatchProps;
@ -27,6 +30,7 @@ interface PatchRuleOptions {
}
export const patchRule = async ({
actionsClient,
rulesClient,
prebuiltRuleAssetClient,
rulePatch,
@ -55,7 +59,7 @@ export const patchRule = async ({
const patchedInternalRule = await rulesClient.update({
id: existingRule.id,
data: convertRuleResponseToAlertingRule(patchedRule),
data: convertRuleResponseToAlertingRule(patchedRule, actionsClient),
});
const { enabled } = await toggleRuleEnabledOnUpdate(rulesClient, existingRule, patchedRule);

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
@ -20,6 +21,7 @@ import { getRuleByIdOrRuleId } from './get_rule_by_id_or_rule_id';
import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response';
interface UpdateRuleArguments {
actionsClient: ActionsClient;
rulesClient: RulesClient;
prebuiltRuleAssetClient: IPrebuiltRuleAssetsClient;
ruleUpdate: RuleUpdateProps;
@ -27,6 +29,7 @@ interface UpdateRuleArguments {
}
export const updateRule = async ({
actionsClient,
rulesClient,
prebuiltRuleAssetClient,
ruleUpdate,
@ -55,7 +58,7 @@ export const updateRule = async ({
const updatedRule = await rulesClient.update({
id: existingRule.id,
data: convertRuleResponseToAlertingRule(ruleWithUpdates),
data: convertRuleResponseToAlertingRule(ruleWithUpdates, actionsClient),
});
const { enabled } = await toggleRuleEnabledOnUpdate(rulesClient, existingRule, ruleWithUpdates);

View file

@ -6,6 +6,8 @@
*/
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
import type { MlAuthz } from '../../../../../machine_learning/authz';
import type { PrebuiltRuleAsset } from '../../../../prebuilt_rules';
@ -18,11 +20,13 @@ import { createRule } from './create_rule';
import { getRuleByRuleId } from './get_rule_by_rule_id';
export const upgradePrebuiltRule = async ({
actionsClient,
rulesClient,
ruleAsset,
mlAuthz,
prebuiltRuleAssetClient,
}: {
actionsClient: ActionsClient;
rulesClient: RulesClient;
ruleAsset: PrebuiltRuleAsset;
mlAuthz: MlAuthz;
@ -46,6 +50,7 @@ export const upgradePrebuiltRule = async ({
await rulesClient.delete({ id: existingRule.id });
const createdRule = await createRule({
actionsClient,
rulesClient,
mlAuthz,
rule: {
@ -72,7 +77,7 @@ export const upgradePrebuiltRule = async ({
const patchedInternalRule = await rulesClient.update({
id: existingRule.id,
data: convertRuleResponseToAlertingRule(patchedRule),
data: convertRuleResponseToAlertingRule(patchedRule, actionsClient),
});
return convertAlertingRuleToRuleResponse(patchedInternalRule);

View file

@ -47,9 +47,9 @@ const filterOutPredefinedActionConnectorsIds = async (
actionsClient: ActionsClient,
actionsIdsToExport: string[]
): Promise<string[]> => {
const allActions = await actionsClient.getAll();
const allActions = await actionsClient.getAll({ includeSystemActions: true });
const predefinedActionsIds = allActions
.filter(({ isPreconfigured }) => isPreconfigured)
.filter(({ isPreconfigured, isSystemAction }) => isPreconfigured || isSystemAction)
.map(({ id }) => id);
if (predefinedActionsIds.length)
return actionsIdsToExport.filter((id) => !predefinedActionsIds.includes(id));
@ -77,6 +77,7 @@ export const getRuleActionConnectorsForExport = async (
};
let actionsIds = [...new Set(rules.flatMap((rule) => rule.actions.map(({ id }) => id)))];
if (!actionsIds.length) return exportedActionConnectors;
// handle preconfigured connectors

View file

@ -105,9 +105,9 @@ export const importRuleActionConnectors = async ({
async function fetchPreconfiguredActionConnectors(
actionsClient: ActionsClient
): Promise<ConnectorWithExtraFindData[]> {
const knownConnectors = await actionsClient.getAll();
const knownConnectors = await actionsClient.getAll({ includeSystemActions: true });
return knownConnectors.filter((c) => c.isPreconfigured);
return knownConnectors.filter((c) => c.isPreconfigured || c.isSystemAction);
}
async function filterOutPreconfiguredConnectors(

View file

@ -10,6 +10,7 @@ import { Readable } from 'stream';
import { createPromiseFromStreams } from '@kbn/utils';
import type { RuleAction, ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types';
import type { PartialRule } from '@kbn/alerting-plugin/server';
import type { ActionsClient } from '@kbn/actions-plugin/server';
import type { RuleToImport } from '../../../../../common/api/detection_engine/rule_management';
import { getCreateRulesSchemaMock } from '../../../../../common/api/detection_engine/model/rule_schema/mocks';
@ -58,6 +59,9 @@ const createMockImportRule = async (rule: ReturnType<typeof getCreateRulesSchema
describe('utils', () => {
const { clients } = requestContextMock.createTools();
const actionsClient = {
isSystemAction: jest.fn((id: string) => id === 'system-connector-.cases'),
} as unknown as jest.Mocked<ActionsClient>;
describe('internalRuleToAPIResponse', () => {
test('should work with a full data set', () => {
@ -627,7 +631,8 @@ describe('utils', () => {
const res = await migrateLegacyActionsIds(
// @ts-expect-error
[rule],
soClient
soClient,
actionsClient
);
expect(res).toEqual([{ ...rule, actions: [{ ...mockAction, id: 'new-post-8.0-id' }] }]);
});
@ -649,7 +654,8 @@ describe('utils', () => {
const res = await migrateLegacyActionsIds(
// @ts-expect-error
[rule],
soClient
soClient,
actionsClient
);
expect(res).toEqual([
{
@ -679,7 +685,7 @@ describe('utils', () => {
soClient.find.mockRejectedValueOnce(new Error('failed to query'));
const res = await migrateLegacyActionsIds(rules, soClient);
const res = await migrateLegacyActionsIds(rules, soClient, actionsClient);
expect(soClient.find.mock.calls).toHaveLength(2);
const [error, ruleRes] = partition<PromiseFromStreams, Error>(
(item): item is Error => item instanceof Error
@ -722,7 +728,8 @@ describe('utils', () => {
const res = await migrateLegacyActionsIds(
// @ts-expect-error
[rule],
soClient
soClient,
actionsClient
);
expect(res[1] instanceof Error).toBeTruthy();
expect((res[1] as unknown as Error).message).toEqual(
@ -764,7 +771,8 @@ describe('utils', () => {
const res = await migrateLegacyActionsIds(
// @ts-expect-error
[rule, rule],
soClient
soClient,
actionsClient
);
expect(res[0]).toEqual({ ...rule, actions: [{ ...mockAction, id: 'new-post-8.0-id' }] });
expect(res[1]).toEqual({ ...rule, actions: [] });
@ -780,6 +788,25 @@ describe('utils', () => {
})
);
});
test('does not migrate system actions', async () => {
const mockSystemAction: RuleAction = {
group: 'group string',
id: 'system-connector-.cases',
action_type_id: '.case',
params: {},
};
const rule: ReturnType<typeof getCreateRulesSchemaMock> = {
...getCreateRulesSchemaMock('rule-1'),
actions: [mockSystemAction],
};
const res = await migrateLegacyActionsIds(
// @ts-expect-error
[rule],
soClient,
actionsClient
);
expect(res).toEqual([{ ...rule, actions: [{ ...mockSystemAction }] }]);
});
});
describe('getInvalidConnectors', () => {
beforeEach(() => {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { partition } from 'lodash/fp';
import { partition, isEmpty } from 'lodash/fp';
import pMap from 'p-map';
import { v4 as uuidv4 } from 'uuid';
@ -21,6 +21,7 @@ import type {
RequiredField,
RequiredFieldInput,
RuleResponse,
RuleAction as RuleActionSchema,
} from '../../../../../common/api/detection_engine/model/rule_schema';
import type {
FindRulesResponse,
@ -222,18 +223,23 @@ export const swapActionIds = async (
*/
export const migrateLegacyActionsIds = async (
rules: PromiseFromStreams[],
savedObjectsClient: SavedObjectsClientContract
savedObjectsClient: SavedObjectsClientContract,
actionsClient: ActionsClient
): Promise<PromiseFromStreams[]> => {
const isImportRule = (r: unknown): r is RuleToImport => !(r instanceof Error);
const toReturn = await pMap(
rules,
async (rule) => {
if (isImportRule(rule)) {
if (isImportRule(rule) && rule.actions != null && !isEmpty(rule.actions)) {
// filter out system actions, since they were not part of any 7.x releases and do not need to be migrated
const [systemActions, extActions] = partition<RuleAction>((action) =>
actionsClient.isSystemAction(action.id)
)(rule.actions);
// can we swap the pre 8.0 action connector(s) id with the new,
// post-8.0 action id (swap the originId for the new _id?)
const newActions: Array<RuleAction | Error> = await pMap(
(rule.actions as RuleAction[]) ?? [],
(extActions as RuleAction[]) ?? [],
(action: RuleAction) => swapActionIds(action, savedObjectsClient),
{ concurrency: MAX_CONCURRENT_SEARCHES }
);
@ -244,11 +250,11 @@ export const migrateLegacyActionsIds = async (
)(newActions);
if (actionMigrationErrors == null || actionMigrationErrors.length === 0) {
return { ...rule, actions: newlyMigratedActions };
return { ...rule, actions: [...newlyMigratedActions, ...systemActions] };
}
return [
{ ...rule, actions: newlyMigratedActions },
{ ...rule, actions: [...newlyMigratedActions, ...systemActions] },
new Error(
JSON.stringify(
createBulkErrorObject({
@ -384,3 +390,11 @@ export const addEcsToRequiredFields = (requiredFields?: RequiredFieldInput[]): R
ecs: isEcsField,
};
});
export const separateActionsAndSystemAction = (
actionsClient: ActionsClient,
actions: RuleActionSchema[] | undefined
) =>
!isEmpty(actions)
? partition((action: RuleActionSchema) => actionsClient.isSystemAction(action.id))(actions)
: [[], actions];

View file

@ -107,6 +107,7 @@ export const previewRulesRoute = (
const searchSourceClient = await data.search.searchSource.asScoped(request);
const savedObjectsClient = coreContext.savedObjects.client;
const siemClient = (await context.securitySolution).getAppClient();
const actionsClient = (await context.actions).getActionsClient();
const timeframeEnd = request.body.timeframeEnd;
let invocationCount = request.body.invocationCount;
@ -120,7 +121,10 @@ export const previewRulesRoute = (
});
}
const internalRule = convertRuleResponseToAlertingRule(applyRuleDefaults(request.body));
const internalRule = convertRuleResponseToAlertingRule(
applyRuleDefaults(request.body),
actionsClient
);
const previewRuleParams = internalRule.params;
const mlAuthz = buildMlAuthz({

View file

@ -69,6 +69,7 @@ export class RequestContextFactory implements IRequestContextFactory {
const frameworkRequest = await buildFrameworkRequest(context, request);
const coreContext = await context.core;
const licensing = await context.licensing;
const actionsClient = await startPlugins.actions.getActionsClientWithRequest(request);
const getSpaceId = (): string =>
startPlugins.spaces?.spacesService?.getSpaceId(request) || DEFAULT_SPACE_ID;
@ -123,6 +124,7 @@ export class RequestContextFactory implements IRequestContextFactory {
});
return createDetectionRulesClient({
actionsClient,
rulesClient: startPlugins.alerting.getRulesClientWithRequest(request),
savedObjectsClient: coreContext.savedObjects.client,
mlAuthz,

View file

@ -7,12 +7,14 @@
import { CASES_URL } from '@kbn/cases-plugin/common';
import { Case } from '@kbn/cases-plugin/common/types/domain';
import { CasePostRequest } from '@kbn/cases-plugin/common/types/api';
import { CasePostRequest, CasesFindResponse } from '@kbn/cases-plugin/common/types/api';
import type SuperTest from 'supertest';
import { ToolingLog } from '@kbn/tooling-log';
import { User } from '../authentication/types';
import { superUser } from '../authentication/users';
import { getSpaceUrlPrefix, setupAuth } from './helpers';
import { waitFor } from '../../../../common/utils/security_solution/detections_response';
export const createCase = async (
supertest: SuperTest.Agent,
@ -35,6 +37,34 @@ export const createCase = async (
return theCase;
};
export const waitForCases = async (supertest: SuperTest.Agent, log: ToolingLog): Promise<void> => {
await waitFor(
async () => {
const response = await getCases(supertest);
const cases = response;
return cases != null && cases.cases.length > 0 && cases?.cases[0]?.totalAlerts > 0;
},
'waitForCaseToAttachAlert',
log
);
};
export const getCases = async (
supertest: SuperTest.Agent,
expectedHttpCode: number = 200,
headers: Record<string, string | string[]> = {}
): Promise<CasesFindResponse> => {
const { body: theCase } = await supertest
.get(`${CASES_URL}/_find`)
.set('kbn-xsrf', 'true')
.set('x-elastic-internal-origin', 'foo')
.set('elastic-api-version', '2023-10-31')
.set(headers)
.expect(expectedHttpCode);
return theCase;
};
/**
* Sends a delete request for the specified case IDs.
*/

View file

@ -18,6 +18,7 @@ interface CreateTestConfigOptions {
// test.not-enabled is specifically not enabled
const enabledActionTypes = [
'.cases',
'.email',
'.index',
'.pagerduty',

View file

@ -8,21 +8,56 @@
import expect from 'expect';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants';
import { QueryRuleCreateProps } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { getCases, waitForCases } from '../../../../../../cases_api_integration/common/lib/api';
import {
deleteAllRules,
waitForRuleSuccess,
deleteAllAlerts,
getRuleForAlertTesting,
createRule,
} from '../../../../../../common/utils/security_solution';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
import { createWebHookRuleAction, fetchRule, getCustomQueryRuleParams } from '../../../utils';
import {
createWebHookRuleAction,
fetchRule,
getAlerts,
getCustomQueryRuleParams,
} from '../../../utils';
import { EsArchivePathBuilder } from '../../../../../es_archive_path_builder';
/**
* Specific _id to use for some of the tests. If the archiver changes and you see errors
* here, update this to a new value of a chosen auditbeat record and update the tests values.
*/
const ID = 'BhbXBmkBR346wHgn4PeZ';
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const log = getService('log');
const esArchiver = getService('esArchiver');
const es = getService('es');
const log = getService('log');
// TODO: add a new service for loading archiver files similar to "getService('es')"
const config = getService('config');
const isServerless = config.get('serverless');
const dataPathBuilder = new EsArchivePathBuilder(isServerless);
const auditbeatPath = dataPathBuilder.getPath('auditbeat/hosts');
describe('@serverless @serverlessQA @ess add_actions', () => {
describe('adding actions', () => {
before(async () => {
await esArchiver.load(auditbeatPath);
await esArchiver.load('x-pack/test/functional/es_archives/security_solution/alerts/8.8.0', {
useCreate: true,
docsOnly: true,
});
});
after(async () => {
await esArchiver.unload(auditbeatPath);
await esArchiver.unload(
'x-pack/test/functional/es_archives/signals/severity_risk_overrides'
);
});
beforeEach(async () => {
await es.indices.delete({ index: 'logs-test', ignore_unavailable: true });
await es.indices.create({
@ -39,6 +74,34 @@ export default ({ getService }: FtrProviderContext) => {
await deleteAllRules(supertest, log);
});
it('should create a case if a rule with the cases system action finds matching alerts', async () => {
const rule: QueryRuleCreateProps = {
...getRuleForAlertTesting(['auditbeat-*']),
query: `_id:${ID}`,
actions: [
{
id: 'system-connector-.cases',
params: {
subAction: 'run',
subActionParams: {
timeWindow: '7d',
reopenClosedCases: false,
groupingBy: ['agent.name'],
},
},
action_type_id: '.cases',
},
],
};
const createdRule = await createRule(supertest, log, rule);
const alerts = await getAlerts(supertest, log, es, createdRule);
await waitForCases(supertest, log);
const cases = await getCases(supertest);
expect(cases.cases[0].totalAlerts).toBeGreaterThan(0);
expect(alerts.hits.hits.length).toBeGreaterThan(0);
expect(alerts.hits.hits[0]._source?.['kibana.alert.ancestors'][0].id).toEqual(ID);
});
it('creates rule with a new webhook action', async () => {
const webhookAction = await createWebHookRuleAction(supertest);
const ruleAction = {

View file

@ -15,6 +15,7 @@ import {
SERVER_LOG_ACTION_BTN,
XMATTERS_ACTION_BTN,
ACTION_BTN,
CASES_SYSTEM_ACTION_BTN,
} from '../../../../screens/common/rule_actions';
import { createRule } from '../../../../tasks/api_calls/rules';
@ -66,6 +67,7 @@ describe(
cy.get(WEBHOOK_ACTION_BTN).should('be.visible');
cy.get(SERVER_LOG_ACTION_BTN).should('be.visible');
cy.get(XMATTERS_ACTION_BTN).should('be.visible');
cy.get(CASES_SYSTEM_ACTION_BTN).should('be.visible');
});
}
);

View file

@ -15,6 +15,7 @@ import {
XMATTERS_ACTION_BTN,
SERVER_LOG_ACTION_BTN,
ACTION_BTN,
CASES_SYSTEM_ACTION_BTN,
} from '../../../../screens/common/rule_actions';
import { createRule } from '../../../../tasks/api_calls/rules';
@ -68,6 +69,7 @@ describe(
cy.get(WEBHOOK_ACTION_BTN).should('not.exist');
cy.get(XMATTERS_ACTION_BTN).should('not.exist');
cy.get(SERVER_LOG_ACTION_BTN).should('not.exist');
cy.get(CASES_SYSTEM_ACTION_BTN).should('not.exist');
});
}
);

View file

@ -13,6 +13,8 @@ export const SERVER_LOG_ACTION_BTN = '[data-test-subj=".server-log-siem-ActionTy
export const XMATTERS_ACTION_BTN = '[data-test-subj=".xmatters-siem-ActionTypeSelectOption"]';
export const CASES_SYSTEM_ACTION_BTN = '[data-test-subj=".cases-siem-ActionTypeSelectOption"]';
/**
* all rule actions buttons, elements which data-test-subj attribute ends with '-siem-ActionTypeSelectOption'
*/