mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
1aee15f611
commit
d7493052e2
54 changed files with 1302 additions and 128 deletions
|
@ -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: {} } }) => {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -543,7 +543,6 @@ components:
|
|||
$ref: '#/components/schemas/RuleActionFrequency'
|
||||
required:
|
||||
- action_type_id
|
||||
- group
|
||||
- id
|
||||
- params
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -328,7 +328,6 @@ components:
|
|||
alerts_filter:
|
||||
$ref: '../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/RuleActionAlertsFilter'
|
||||
required:
|
||||
- group
|
||||
- id
|
||||
- params
|
||||
additionalProperties: false
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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 = ({
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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' }
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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
|
||||
),
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 ?? [],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
),
|
||||
];
|
||||
};
|
||||
|
|
|
@ -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 }),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -18,6 +18,7 @@ interface CreateTestConfigOptions {
|
|||
|
||||
// test.not-enabled is specifically not enabled
|
||||
const enabledActionTypes = [
|
||||
'.cases',
|
||||
'.email',
|
||||
'.index',
|
||||
'.pagerduty',
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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'
|
||||
*/
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue