mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[EDR Workflows] Automated Actions in more rule types (#191874)
This commit is contained in:
parent
70b7d26335
commit
004631b6c2
33 changed files with 737 additions and 85 deletions
|
@ -6135,6 +6135,175 @@ Object {
|
|||
"query": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"responseActions": Object {
|
||||
"items": Object {
|
||||
"anyOf": Array [
|
||||
Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"actionTypeId": Object {
|
||||
"const": ".osquery",
|
||||
"type": "string",
|
||||
},
|
||||
"params": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"ecsMapping": Object {
|
||||
"additionalProperties": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"field": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"value": Object {
|
||||
"anyOf": Array [
|
||||
Object {
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"items": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
"properties": Object {},
|
||||
"type": "object",
|
||||
},
|
||||
"packId": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"queries": Object {
|
||||
"items": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"ecs_mapping": Object {
|
||||
"$ref": "#/allOf/1/properties/responseActions/items/anyOf/0/properties/params/properties/ecsMapping",
|
||||
},
|
||||
"id": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"platform": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"query": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"removed": Object {
|
||||
"type": "boolean",
|
||||
},
|
||||
"snapshot": Object {
|
||||
"type": "boolean",
|
||||
},
|
||||
"version": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"id",
|
||||
"query",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"query": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"savedQueryId": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"timeout": Object {
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"actionTypeId",
|
||||
"params",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"actionTypeId": Object {
|
||||
"const": ".endpoint",
|
||||
"type": "string",
|
||||
},
|
||||
"params": Object {
|
||||
"anyOf": Array [
|
||||
Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"command": Object {
|
||||
"const": "isolate",
|
||||
"type": "string",
|
||||
},
|
||||
"comment": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"command",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"command": Object {
|
||||
"enum": Array [
|
||||
"kill-process",
|
||||
"suspend-process",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"comment": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"config": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"field": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"overwrite": Object {
|
||||
"default": true,
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"field",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"command",
|
||||
"config",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"actionTypeId",
|
||||
"params",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"tiebreakerField": Object {
|
||||
"type": "string",
|
||||
},
|
||||
|
@ -7687,6 +7856,175 @@ Object {
|
|||
"query": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"responseActions": Object {
|
||||
"items": Object {
|
||||
"anyOf": Array [
|
||||
Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"actionTypeId": Object {
|
||||
"const": ".osquery",
|
||||
"type": "string",
|
||||
},
|
||||
"params": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"ecsMapping": Object {
|
||||
"additionalProperties": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"field": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"value": Object {
|
||||
"anyOf": Array [
|
||||
Object {
|
||||
"type": "string",
|
||||
},
|
||||
Object {
|
||||
"items": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
"properties": Object {},
|
||||
"type": "object",
|
||||
},
|
||||
"packId": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"queries": Object {
|
||||
"items": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"ecs_mapping": Object {
|
||||
"$ref": "#/allOf/1/properties/responseActions/items/anyOf/0/properties/params/properties/ecsMapping",
|
||||
},
|
||||
"id": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"platform": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"query": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"removed": Object {
|
||||
"type": "boolean",
|
||||
},
|
||||
"snapshot": Object {
|
||||
"type": "boolean",
|
||||
},
|
||||
"version": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"id",
|
||||
"query",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"query": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"savedQueryId": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"timeout": Object {
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"actionTypeId",
|
||||
"params",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"actionTypeId": Object {
|
||||
"const": ".endpoint",
|
||||
"type": "string",
|
||||
},
|
||||
"params": Object {
|
||||
"anyOf": Array [
|
||||
Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"command": Object {
|
||||
"const": "isolate",
|
||||
"type": "string",
|
||||
},
|
||||
"comment": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"command",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"command": Object {
|
||||
"enum": Array [
|
||||
"kill-process",
|
||||
"suspend-process",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"comment": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"config": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"field": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"overwrite": Object {
|
||||
"default": true,
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"field",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"command",
|
||||
"config",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"actionTypeId",
|
||||
"params",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"type": Object {
|
||||
"const": "new_terms",
|
||||
"type": "string",
|
||||
|
|
|
@ -224,6 +224,7 @@ export const EqlOptionalFields = z.object({
|
|||
tiebreaker_field: TiebreakerField.optional(),
|
||||
timestamp_field: TimestampField.optional(),
|
||||
alert_suppression: AlertSuppression.optional(),
|
||||
response_actions: z.array(ResponseAction).optional(),
|
||||
});
|
||||
|
||||
export type EqlRuleCreateFields = z.infer<typeof EqlRuleCreateFields>;
|
||||
|
@ -521,6 +522,7 @@ export const NewTermsRuleOptionalFields = z.object({
|
|||
data_view_id: DataViewId.optional(),
|
||||
filters: RuleFilterArray.optional(),
|
||||
alert_suppression: AlertSuppression.optional(),
|
||||
response_actions: z.array(ResponseAction).optional(),
|
||||
});
|
||||
|
||||
export type NewTermsRuleDefaultableFields = z.infer<typeof NewTermsRuleDefaultableFields>;
|
||||
|
@ -574,6 +576,7 @@ export const EsqlRuleRequiredFields = z.object({
|
|||
export type EsqlRuleOptionalFields = z.infer<typeof EsqlRuleOptionalFields>;
|
||||
export const EsqlRuleOptionalFields = z.object({
|
||||
alert_suppression: AlertSuppression.optional(),
|
||||
response_actions: z.array(ResponseAction).optional(),
|
||||
});
|
||||
|
||||
export type EsqlRulePatchFields = z.infer<typeof EsqlRulePatchFields>;
|
||||
|
|
|
@ -292,6 +292,10 @@ components:
|
|||
$ref: './specific_attributes/eql_attributes.schema.yaml#/components/schemas/TimestampField'
|
||||
alert_suppression:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/AlertSuppression'
|
||||
response_actions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../rule_response_actions/response_actions.schema.yaml#/components/schemas/ResponseAction'
|
||||
|
||||
EqlRuleCreateFields:
|
||||
allOf:
|
||||
|
@ -762,6 +766,10 @@ components:
|
|||
$ref: './common_attributes.schema.yaml#/components/schemas/RuleFilterArray'
|
||||
alert_suppression:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/AlertSuppression'
|
||||
response_actions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../rule_response_actions/response_actions.schema.yaml#/components/schemas/ResponseAction'
|
||||
|
||||
NewTermsRuleDefaultableFields:
|
||||
type: object
|
||||
|
@ -840,6 +848,10 @@ components:
|
|||
properties:
|
||||
alert_suppression:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/AlertSuppression'
|
||||
response_actions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../rule_response_actions/response_actions.schema.yaml#/components/schemas/ResponseAction'
|
||||
|
||||
EsqlRulePatchFields:
|
||||
allOf:
|
||||
|
|
|
@ -93,3 +93,14 @@ export const isSuppressionRuleConfiguredWithMissingFields = (ruleType: Type) =>
|
|||
export const isSuppressionRuleInGA = (ruleType: Type): boolean => {
|
||||
return isSuppressibleAlertRule(ruleType) && SUPPRESSIBLE_ALERT_RULES_GA.includes(ruleType);
|
||||
};
|
||||
|
||||
export const shouldShowResponseActions = (
|
||||
ruleType: Type | undefined,
|
||||
automatedResponseActionsForMoreRulesEnabled: boolean
|
||||
) => {
|
||||
return (
|
||||
isQueryRule(ruleType) ||
|
||||
(automatedResponseActionsForMoreRulesEnabled &&
|
||||
(isEsqlRule(ruleType) || isEqlRule(ruleType) || isNewTermsRule(ruleType)))
|
||||
);
|
||||
};
|
||||
|
|
|
@ -52,6 +52,11 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
*/
|
||||
automatedProcessActionsEnabled: true,
|
||||
|
||||
/**
|
||||
* Temporary feature flag to enable the Response Actions in Rules UI - intermediate release
|
||||
*/
|
||||
automatedResponseActionsForMoreRulesEnabled: false,
|
||||
|
||||
/**
|
||||
* Enables the ability to send Response actions to SentinelOne and persist the results
|
||||
* in ES. Adds API changes to support `agentType` and supports `isolate` and `release`
|
||||
|
|
|
@ -2042,6 +2042,10 @@ components:
|
|||
$ref: '#/components/schemas/RuleFilterArray'
|
||||
index:
|
||||
$ref: '#/components/schemas/IndexPatternArray'
|
||||
response_actions:
|
||||
items:
|
||||
$ref: '#/components/schemas/ResponseAction'
|
||||
type: array
|
||||
tiebreaker_field:
|
||||
$ref: '#/components/schemas/TiebreakerField'
|
||||
timestamp_field:
|
||||
|
@ -2729,6 +2733,10 @@ components:
|
|||
properties:
|
||||
alert_suppression:
|
||||
$ref: '#/components/schemas/AlertSuppression'
|
||||
response_actions:
|
||||
items:
|
||||
$ref: '#/components/schemas/ResponseAction'
|
||||
type: array
|
||||
EsqlRulePatchProps:
|
||||
allOf:
|
||||
- type: object
|
||||
|
@ -3873,6 +3881,10 @@ components:
|
|||
$ref: '#/components/schemas/RuleFilterArray'
|
||||
index:
|
||||
$ref: '#/components/schemas/IndexPatternArray'
|
||||
response_actions:
|
||||
items:
|
||||
$ref: '#/components/schemas/ResponseAction'
|
||||
type: array
|
||||
NewTermsRulePatchFields:
|
||||
allOf:
|
||||
- type: object
|
||||
|
|
|
@ -1316,6 +1316,10 @@ components:
|
|||
$ref: '#/components/schemas/RuleFilterArray'
|
||||
index:
|
||||
$ref: '#/components/schemas/IndexPatternArray'
|
||||
response_actions:
|
||||
items:
|
||||
$ref: '#/components/schemas/ResponseAction'
|
||||
type: array
|
||||
tiebreaker_field:
|
||||
$ref: '#/components/schemas/TiebreakerField'
|
||||
timestamp_field:
|
||||
|
@ -2003,6 +2007,10 @@ components:
|
|||
properties:
|
||||
alert_suppression:
|
||||
$ref: '#/components/schemas/AlertSuppression'
|
||||
response_actions:
|
||||
items:
|
||||
$ref: '#/components/schemas/ResponseAction'
|
||||
type: array
|
||||
EsqlRulePatchProps:
|
||||
allOf:
|
||||
- type: object
|
||||
|
@ -3026,6 +3034,10 @@ components:
|
|||
$ref: '#/components/schemas/RuleFilterArray'
|
||||
index:
|
||||
$ref: '#/components/schemas/IndexPatternArray'
|
||||
response_actions:
|
||||
items:
|
||||
$ref: '#/components/schemas/ResponseAction'
|
||||
type: array
|
||||
NewTermsRulePatchFields:
|
||||
allOf:
|
||||
- type: object
|
||||
|
|
|
@ -16,8 +16,9 @@ import type {
|
|||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { UseArray } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { shouldShowResponseActions } from '../../../../../common/detection_engine/utils';
|
||||
import type { RuleObjectId } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { isQueryRule } from '../../../../../common/detection_engine/utils';
|
||||
import { ResponseActionsForm } from '../../../rule_response_actions/response_actions_form';
|
||||
import type {
|
||||
RuleStepProps,
|
||||
|
@ -84,6 +85,9 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
|||
const {
|
||||
services: { application },
|
||||
} = useKibana();
|
||||
const automatedResponseActionsForMoreRulesEnabled = useIsExperimentalFeatureEnabled(
|
||||
'automatedResponseActionsForMoreRulesEnabled'
|
||||
);
|
||||
const displayActionsOptions = useMemo(
|
||||
() => (
|
||||
<>
|
||||
|
@ -101,7 +105,7 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
|||
[actionMessageParams, summaryActionMessageParams]
|
||||
);
|
||||
const displayResponseActionsOptions = useMemo(() => {
|
||||
if (isQueryRule(ruleType)) {
|
||||
if (shouldShowResponseActions(ruleType, automatedResponseActionsForMoreRulesEnabled)) {
|
||||
return (
|
||||
<UseArray path="responseActions" initialNumberOfItems={0}>
|
||||
{ResponseActionsForm}
|
||||
|
@ -109,7 +113,7 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
|
|||
);
|
||||
}
|
||||
return null;
|
||||
}, [ruleType]);
|
||||
}, [ruleType, automatedResponseActionsForMoreRulesEnabled]);
|
||||
// only display the actions dropdown if the user has "read" privileges for actions
|
||||
const displayActionsDropDown = useMemo(() => {
|
||||
return application.capabilities.actions.show ? (
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
tryAddingDisabledResponseAction,
|
||||
validateAvailableCommands,
|
||||
visitRuleActions,
|
||||
selectIsolateAndSaveWithoutEnabling,
|
||||
fillUpNewEsqlRule,
|
||||
} from '../../tasks/response_actions';
|
||||
import { cleanupRule, generateRandomStringName, loadRule } from '../../tasks/api_fixtures';
|
||||
import { ResponseActionTypesEnum } from '../../../../../common/api/detection_engine';
|
||||
|
@ -28,6 +30,7 @@ describe(
|
|||
kbnServerArgs: [
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'automatedProcessActionsEnabled',
|
||||
'automatedResponseActionsForMoreRulesEnabled',
|
||||
])}`,
|
||||
],
|
||||
},
|
||||
|
@ -202,6 +205,23 @@ describe(
|
|||
});
|
||||
});
|
||||
|
||||
describe('User should be able to add response action to ESQL rule', () => {
|
||||
const [ruleName, ruleDescription] = generateRandomStringName(2);
|
||||
|
||||
beforeEach(() => {
|
||||
login(ROLE.soc_manager);
|
||||
});
|
||||
|
||||
it('create and save endpoint response action inside of a rule', () => {
|
||||
const query = 'FROM * METADATA _index, _id';
|
||||
fillUpNewEsqlRule(ruleName, ruleDescription, query);
|
||||
addEndpointResponseAction();
|
||||
focusAndOpenCommandDropdown();
|
||||
validateAvailableCommands();
|
||||
selectIsolateAndSaveWithoutEnabling(ruleName);
|
||||
});
|
||||
});
|
||||
|
||||
describe('User should not see endpoint action when no rbac', () => {
|
||||
const [ruleName, ruleDescription] = generateRandomStringName(2);
|
||||
|
||||
|
|
|
@ -42,6 +42,12 @@ export const validateAvailableCommands = () => {
|
|||
cy.getByTestSubj(`command-type-${command}`);
|
||||
});
|
||||
};
|
||||
export const selectIsolateAndSaveWithoutEnabling = (ruleName: string) => {
|
||||
cy.getByTestSubj(`command-type-isolate`).click();
|
||||
cy.getByTestSubj('create-enabled-false').click();
|
||||
cy.contains(`${ruleName} was created`);
|
||||
};
|
||||
|
||||
export const addEndpointResponseAction = () => {
|
||||
cy.getByTestSubj('response-actions-wrapper').within(() => {
|
||||
cy.getByTestSubj('Elastic Defend-response-action-type-selection-option').click();
|
||||
|
@ -69,6 +75,26 @@ export const fillUpNewRule = (name = 'Test', description = 'Test') => {
|
|||
cy.getByTestSubj('about-continue').click();
|
||||
cy.getByTestSubj('schedule-continue').click();
|
||||
};
|
||||
export const fillUpNewEsqlRule = (name = 'Test', description = 'Test', query: string) => {
|
||||
loadPage('app/security/rules/management');
|
||||
cy.getByTestSubj('create-new-rule').click();
|
||||
cy.getByTestSubj('stepDefineRule').within(() => {
|
||||
cy.getByTestSubj('esqlRuleType').click();
|
||||
cy.getByTestSubj('detectionEngineStepDefineRuleEsqlQueryBar').within(() => {
|
||||
cy.getByTestSubj('globalQueryBar').click();
|
||||
cy.getByTestSubj('kibanaCodeEditor').type(query);
|
||||
});
|
||||
});
|
||||
cy.getByTestSubj('define-continue').click();
|
||||
cy.getByTestSubj('detectionEngineStepAboutRuleName').within(() => {
|
||||
cy.getByTestSubj('input').type(name);
|
||||
});
|
||||
cy.getByTestSubj('detectionEngineStepAboutRuleDescription').within(() => {
|
||||
cy.getByTestSubj('input').type(description);
|
||||
});
|
||||
cy.getByTestSubj('about-continue').click();
|
||||
cy.getByTestSubj('schedule-continue').click();
|
||||
};
|
||||
export const visitRuleActions = (ruleId: string) => {
|
||||
loadPage(`app/security/rules/id/${ruleId}/edit`);
|
||||
cy.getByTestSubj('edit-rule-actions-tab').should('exist');
|
||||
|
|
|
@ -51,6 +51,7 @@ describe('Prebuilt rule asset schema', () => {
|
|||
// See: detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts
|
||||
const omittedBaseFields = [
|
||||
'actions',
|
||||
'response_actions',
|
||||
'throttle',
|
||||
'meta',
|
||||
'output_index',
|
||||
|
|
|
@ -63,14 +63,14 @@ const TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_SAVED_QUERY_RULES =
|
|||
|
||||
export type TypeSpecificFields = z.infer<typeof TypeSpecificFields>;
|
||||
export const TypeSpecificFields = z.discriminatedUnion('type', [
|
||||
EqlRuleCreateFields,
|
||||
EqlRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES),
|
||||
QueryRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES),
|
||||
SavedQueryRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_SAVED_QUERY_RULES),
|
||||
ThresholdRuleCreateFields,
|
||||
ThreatMatchRuleCreateFields,
|
||||
MachineLearningRuleCreateFields,
|
||||
NewTermsRuleCreateFields,
|
||||
EsqlRuleCreateFields,
|
||||
NewTermsRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES),
|
||||
EsqlRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES),
|
||||
]);
|
||||
|
||||
// Make sure the type-specific fields contain all the same rule types as the type-specific rule params.
|
||||
|
|
|
@ -16,7 +16,12 @@ import {
|
|||
} from '../../../../routes/__mocks__/request_responses';
|
||||
import { requestContextMock, serverMock, requestMock } from '../../../../routes/__mocks__';
|
||||
import { createRuleRoute } from './route';
|
||||
import { getCreateRulesSchemaMock } from '../../../../../../../common/api/detection_engine/model/rule_schema/mocks';
|
||||
import {
|
||||
getCreateEqlRuleSchemaMock,
|
||||
getCreateEsqlRulesSchemaMock,
|
||||
getCreateNewTermsRulesSchemaMock,
|
||||
getCreateRulesSchemaMock,
|
||||
} from '../../../../../../../common/api/detection_engine/model/rule_schema/mocks';
|
||||
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
|
||||
import { getQueryRuleParams } from '../../../../rule_schema/mocks';
|
||||
import { HttpAuthzError } from '../../../../../machine_learning/validation';
|
||||
|
@ -181,20 +186,29 @@ describe('Create rule route', () => {
|
|||
},
|
||||
});
|
||||
const defaultAction = getResponseAction();
|
||||
const ruleTypes: Array<[string, () => object]> = [
|
||||
['query', getCreateRulesSchemaMock],
|
||||
['esql', getCreateEsqlRulesSchemaMock],
|
||||
['eql', getCreateEqlRuleSchemaMock],
|
||||
['new_terms', getCreateNewTermsRulesSchemaMock],
|
||||
];
|
||||
|
||||
test('is successful', async () => {
|
||||
const request = requestMock.create({
|
||||
method: 'post',
|
||||
path: DETECTION_ENGINE_RULES_URL,
|
||||
body: {
|
||||
...getCreateRulesSchemaMock(),
|
||||
response_actions: [defaultAction],
|
||||
},
|
||||
});
|
||||
test.each(ruleTypes)(
|
||||
'is successful for %s rule',
|
||||
async (ruleType: string, schemaMock: (ruleId: string) => object) => {
|
||||
const request = requestMock.create({
|
||||
method: 'post',
|
||||
path: DETECTION_ENGINE_RULES_URL,
|
||||
body: {
|
||||
...schemaMock(`rule-${ruleType}`),
|
||||
response_actions: [defaultAction],
|
||||
},
|
||||
});
|
||||
|
||||
const response = await server.inject(request, requestContextMock.convertContext(context));
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
const response = await server.inject(request, requestContextMock.convertContext(context));
|
||||
expect(response.status).toEqual(200);
|
||||
}
|
||||
);
|
||||
|
||||
test('fails when isolate rbac is set to false', async () => {
|
||||
(context.securitySolution.getEndpointAuthz as jest.Mock).mockReturnValue(() => ({
|
||||
|
|
|
@ -17,6 +17,9 @@ import { getRulesSchemaMock } from '../../../../../../../common/api/detection_en
|
|||
import { DETECTION_ENGINE_RULES_URL } from '../../../../../../../common/constants';
|
||||
import { updateRuleRoute } from './route';
|
||||
import {
|
||||
getCreateEqlRuleSchemaMock,
|
||||
getCreateEsqlRulesSchemaMock,
|
||||
getCreateNewTermsRulesSchemaMock,
|
||||
getCreateRulesSchemaMock,
|
||||
getUpdateRulesSchemaMock,
|
||||
} from '../../../../../../../common/api/detection_engine/model/rule_schema/mocks';
|
||||
|
@ -189,19 +192,29 @@ describe('Update rule route', () => {
|
|||
});
|
||||
const defaultAction = getResponseAction();
|
||||
|
||||
test('is successful', async () => {
|
||||
const request = requestMock.create({
|
||||
method: 'post',
|
||||
path: DETECTION_ENGINE_RULES_URL,
|
||||
body: {
|
||||
...getCreateRulesSchemaMock(),
|
||||
response_actions: [defaultAction],
|
||||
},
|
||||
});
|
||||
const ruleTypes: Array<[string, () => object]> = [
|
||||
['query', () => getCreateRulesSchemaMock()],
|
||||
['esql', getCreateEsqlRulesSchemaMock],
|
||||
['eql', getCreateEqlRuleSchemaMock],
|
||||
['new_terms', getCreateNewTermsRulesSchemaMock],
|
||||
];
|
||||
|
||||
const response = await server.inject(request, requestContextMock.convertContext(context));
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
test.each(ruleTypes)(
|
||||
'is successful for %s rule',
|
||||
async (ruleType: string, schemaMock: (ruleId: string) => object) => {
|
||||
const request = requestMock.create({
|
||||
method: 'post',
|
||||
path: DETECTION_ENGINE_RULES_URL,
|
||||
body: {
|
||||
...schemaMock(`rule-${ruleType}`),
|
||||
response_actions: [defaultAction],
|
||||
},
|
||||
});
|
||||
|
||||
const response = await server.inject(request, requestContextMock.convertContext(context));
|
||||
expect(response.status).toEqual(200);
|
||||
}
|
||||
);
|
||||
|
||||
test('fails when isolate rbac is set to false', async () => {
|
||||
(context.securitySolution.getEndpointAuthz as jest.Mock).mockReturnValue(() => ({
|
||||
|
|
|
@ -119,6 +119,9 @@ const typeSpecificSnakeToCamel = (params: TypeSpecificCreateProps): TypeSpecific
|
|||
eventCategoryOverride: params.event_category_override,
|
||||
tiebreakerField: params.tiebreaker_field,
|
||||
alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression),
|
||||
responseActions: params.response_actions?.map((rule) =>
|
||||
transformRuleToAlertResponseAction(rule)
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'esql': {
|
||||
|
@ -127,6 +130,9 @@ const typeSpecificSnakeToCamel = (params: TypeSpecificCreateProps): TypeSpecific
|
|||
language: params.language,
|
||||
query: params.query,
|
||||
alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression),
|
||||
responseActions: params.response_actions?.map((rule) =>
|
||||
transformRuleToAlertResponseAction(rule)
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'threat_match': {
|
||||
|
@ -173,9 +179,6 @@ const typeSpecificSnakeToCamel = (params: TypeSpecificCreateProps): TypeSpecific
|
|||
filters: params.filters,
|
||||
savedId: params.saved_id,
|
||||
dataViewId: params.data_view_id,
|
||||
responseActions: params.response_actions?.map((rule) =>
|
||||
transformRuleToAlertResponseAction(rule)
|
||||
),
|
||||
alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression),
|
||||
};
|
||||
}
|
||||
|
@ -213,6 +216,9 @@ const typeSpecificSnakeToCamel = (params: TypeSpecificCreateProps): TypeSpecific
|
|||
language: params.language ?? 'kuery',
|
||||
dataViewId: params.data_view_id,
|
||||
alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression),
|
||||
responseActions: params.response_actions?.map((rule) =>
|
||||
transformRuleToAlertResponseAction(rule)
|
||||
),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
*/
|
||||
|
||||
import type { RequiredOptional } from '@kbn/zod-helpers';
|
||||
import type { TypeSpecificResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { transformAlertToRuleResponseAction } from '../../../../../../../common/detection_engine/transform_actions';
|
||||
import type { TypeSpecificResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { assertUnreachable } from '../../../../../../../common/utility_types';
|
||||
import { convertObjectKeysToSnakeCase } from '../../../../../../utils/object_case_converters';
|
||||
import type { TypeSpecificRuleParams } from '../../../../rule_schema';
|
||||
|
@ -28,6 +28,7 @@ export const typeSpecificCamelToSnake = (
|
|||
event_category_override: params.eventCategoryOverride,
|
||||
tiebreaker_field: params.tiebreakerField,
|
||||
alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression),
|
||||
response_actions: params.responseActions?.map(transformAlertToRuleResponseAction),
|
||||
};
|
||||
}
|
||||
case 'esql': {
|
||||
|
@ -36,6 +37,7 @@ export const typeSpecificCamelToSnake = (
|
|||
language: params.language,
|
||||
query: params.query,
|
||||
alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression),
|
||||
response_actions: params.responseActions?.map(transformAlertToRuleResponseAction),
|
||||
};
|
||||
}
|
||||
case 'threat_match': {
|
||||
|
@ -118,6 +120,7 @@ export const typeSpecificCamelToSnake = (
|
|||
language: params.language,
|
||||
data_view_id: params.dataViewId,
|
||||
alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression),
|
||||
response_actions: params.responseActions?.map(transformAlertToRuleResponseAction),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
|
|
@ -86,6 +86,7 @@ export const setTypeSpecificDefaults = (props: TypeSpecificCreateProps) => {
|
|||
event_category_override: props.event_category_override,
|
||||
tiebreaker_field: props.tiebreaker_field,
|
||||
alert_suppression: props.alert_suppression,
|
||||
response_actions: props.response_actions,
|
||||
};
|
||||
}
|
||||
case 'esql': {
|
||||
|
@ -94,6 +95,7 @@ export const setTypeSpecificDefaults = (props: TypeSpecificCreateProps) => {
|
|||
language: props.language,
|
||||
query: props.query,
|
||||
alert_suppression: props.alert_suppression,
|
||||
response_actions: props.response_actions,
|
||||
};
|
||||
}
|
||||
case 'threat_match': {
|
||||
|
@ -176,6 +178,7 @@ export const setTypeSpecificDefaults = (props: TypeSpecificCreateProps) => {
|
|||
language: props.language ?? 'kuery',
|
||||
data_view_id: props.data_view_id,
|
||||
alert_suppression: props.alert_suppression,
|
||||
response_actions: props.response_actions,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
|
|
@ -138,6 +138,7 @@ const patchEqlParams = (
|
|||
rulePatch.event_category_override ?? existingRule.event_category_override,
|
||||
tiebreaker_field: rulePatch.tiebreaker_field ?? existingRule.tiebreaker_field,
|
||||
alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression,
|
||||
response_actions: rulePatch.response_actions ?? existingRule.response_actions,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -150,6 +151,7 @@ const patchEsqlParams = (
|
|||
language: rulePatch.language ?? existingRule.language,
|
||||
query: rulePatch.query ?? existingRule.query,
|
||||
alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression,
|
||||
response_actions: rulePatch.response_actions ?? existingRule.response_actions,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -258,6 +260,7 @@ const patchNewTermsParams = (
|
|||
new_terms_fields: params.new_terms_fields ?? existingRule.new_terms_fields,
|
||||
history_window_start: params.history_window_start ?? existingRule.history_window_start,
|
||||
alert_suppression: params.alert_suppression ?? existingRule.alert_suppression,
|
||||
response_actions: params.response_actions ?? existingRule.response_actions,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -9,8 +9,13 @@ import type { PartialRule } from '@kbn/alerting-plugin/server';
|
|||
import type { Rule } from '@kbn/alerting-plugin/common';
|
||||
import { isEqual, xorWith } from 'lodash';
|
||||
import { stringifyZodError } from '@kbn/zod-helpers';
|
||||
import type {
|
||||
EqlRule,
|
||||
EsqlRule,
|
||||
NewTermsRule,
|
||||
QueryRule,
|
||||
} from '../../../../../common/api/detection_engine';
|
||||
import {
|
||||
type QueryRule,
|
||||
type ResponseAction,
|
||||
type RuleCreateProps,
|
||||
RuleResponse,
|
||||
|
@ -21,9 +26,10 @@ import {
|
|||
RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP,
|
||||
RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ,
|
||||
} from '../../../../../common/endpoint/service/response_actions/constants';
|
||||
import { isQueryRule } from '../../../../../common/detection_engine/utils';
|
||||
import { shouldShowResponseActions } from '../../../../../common/detection_engine/utils';
|
||||
import type { SecuritySolutionApiRequestHandlerContext } from '../../../..';
|
||||
import { CustomHttpRequestError } from '../../../../utils/custom_http_request_error';
|
||||
import type { EqlRuleParams, EsqlRuleParams, NewTermsRuleParams } from '../../rule_schema';
|
||||
import {
|
||||
hasValidRuleType,
|
||||
type RuleAlertType,
|
||||
|
@ -64,11 +70,21 @@ export const validateResponseActionsPermissions = async (
|
|||
ruleUpdate: RuleCreateProps | RuleUpdateProps,
|
||||
existingRule?: RuleAlertType | null
|
||||
): Promise<void> => {
|
||||
if (!isQueryRule(ruleUpdate.type)) {
|
||||
const { experimentalFeatures } = await securitySolution.getConfig();
|
||||
|
||||
if (
|
||||
!shouldShowResponseActions(
|
||||
ruleUpdate.type,
|
||||
experimentalFeatures.automatedResponseActionsForMoreRulesEnabled
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isQueryRulePayload(ruleUpdate) || (existingRule && !isQueryRuleObject(existingRule))) {
|
||||
if (
|
||||
!rulePayloadContainsResponseActions(ruleUpdate) ||
|
||||
(existingRule && !ruleObjectContainsResponseActions(existingRule))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -108,10 +124,14 @@ export const validateResponseActionsPermissions = async (
|
|||
});
|
||||
};
|
||||
|
||||
function isQueryRulePayload(rule: RuleCreateProps | RuleUpdateProps): rule is QueryRule {
|
||||
function rulePayloadContainsResponseActions(
|
||||
rule: RuleCreateProps | RuleUpdateProps
|
||||
): rule is QueryRule | EsqlRule | EqlRule | NewTermsRule {
|
||||
return 'response_actions' in rule;
|
||||
}
|
||||
|
||||
function isQueryRuleObject(rule?: RuleAlertType): rule is Rule<UnifiedQueryRuleParams> {
|
||||
function ruleObjectContainsResponseActions(
|
||||
rule?: RuleAlertType
|
||||
): rule is Rule<UnifiedQueryRuleParams | EsqlRuleParams | EqlRuleParams | NewTermsRuleParams> {
|
||||
return rule != null && 'params' in rule && 'responseActions' in rule?.params;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import { each } from 'lodash';
|
||||
import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
|
||||
import { stringify } from '../../../endpoint/utils/stringify';
|
||||
import type {
|
||||
RuleResponseEndpointAction,
|
||||
|
@ -29,8 +28,8 @@ export const endpointResponseAction = async (
|
|||
'ruleExecution',
|
||||
'automatedResponseActions'
|
||||
);
|
||||
const ruleId = alerts[0][ALERT_RULE_UUID];
|
||||
const ruleName = alerts[0][ALERT_RULE_NAME];
|
||||
const ruleId = alerts[0].kibana.alert?.rule.uuid;
|
||||
const ruleName = alerts[0].kibana.alert?.rule.name;
|
||||
const logMsgPrefix = `Rule [${ruleName}][${ruleId}]:`;
|
||||
const { comment, command } = responseAction.params;
|
||||
const errors: string[] = [];
|
||||
|
|
|
@ -96,8 +96,13 @@ describe('ScheduleNotificationResponseActions', () => {
|
|||
},
|
||||
},
|
||||
];
|
||||
await scheduleNotificationResponseActions({ signals, responseActions });
|
||||
const response = await scheduleNotificationResponseActions({
|
||||
signals,
|
||||
signalsCount: signals.length,
|
||||
responseActions,
|
||||
});
|
||||
|
||||
expect(response).not.toBeUndefined();
|
||||
expect(osqueryActionMock.create).toHaveBeenCalledWith({
|
||||
...defaultQueryResultParams,
|
||||
query: simpleQuery,
|
||||
|
@ -123,8 +128,13 @@ describe('ScheduleNotificationResponseActions', () => {
|
|||
},
|
||||
},
|
||||
];
|
||||
await scheduleNotificationResponseActions({ signals, responseActions });
|
||||
const response = await scheduleNotificationResponseActions({
|
||||
signals,
|
||||
signalsCount: signals.length,
|
||||
responseActions,
|
||||
});
|
||||
|
||||
expect(response).not.toBeUndefined();
|
||||
expect(osqueryActionMock.create).toHaveBeenCalledWith({
|
||||
...defaultPackResultParams,
|
||||
queries: [{ ...defaultQueries, id: 'query-1', query: simpleQuery }],
|
||||
|
@ -149,8 +159,12 @@ describe('ScheduleNotificationResponseActions', () => {
|
|||
},
|
||||
},
|
||||
];
|
||||
await scheduleNotificationResponseActions({ signals, responseActions });
|
||||
|
||||
const response = await scheduleNotificationResponseActions({
|
||||
signals,
|
||||
signalsCount: signals.length,
|
||||
responseActions,
|
||||
});
|
||||
expect(response).not.toBeUndefined();
|
||||
expect(endpointActionMock.getInternalResponseActionsClient).toHaveBeenCalledTimes(1);
|
||||
expect(endpointActionMock.getInternalResponseActionsClient).toHaveBeenCalledWith({
|
||||
agentType: 'endpoint',
|
||||
|
@ -188,11 +202,14 @@ describe('ScheduleNotificationResponseActions', () => {
|
|||
},
|
||||
},
|
||||
];
|
||||
await scheduleNotificationResponseActions({
|
||||
const response = await scheduleNotificationResponseActions({
|
||||
signals,
|
||||
signalsCount: signals.length,
|
||||
responseActions,
|
||||
});
|
||||
|
||||
expect(response).not.toBeUndefined();
|
||||
|
||||
expect(mockedResponseActionsClient.killProcess).toHaveBeenCalledWith(
|
||||
{
|
||||
alert_ids: ['alert-id-1'],
|
||||
|
@ -223,12 +240,42 @@ describe('ScheduleNotificationResponseActions', () => {
|
|||
},
|
||||
},
|
||||
];
|
||||
await scheduleNotificationResponseActions({
|
||||
const response = await scheduleNotificationResponseActions({
|
||||
signals,
|
||||
signalsCount: signals.length,
|
||||
responseActions,
|
||||
});
|
||||
|
||||
expect(response).not.toBeUndefined();
|
||||
expect(mockedResponseActionsClient.isolate).toHaveBeenCalledTimes(signals.length - 1);
|
||||
});
|
||||
it('should not call any action service if no response actions are provided', async () => {
|
||||
const response = await scheduleNotificationResponseActions({
|
||||
signals: getSignals(),
|
||||
signalsCount: 2,
|
||||
responseActions: [],
|
||||
});
|
||||
expect(response).toBeUndefined();
|
||||
});
|
||||
it('should not call any action service if signalsCount is 0', async () => {
|
||||
const signals = getSignals();
|
||||
const responseActions: RuleResponseAction[] = [
|
||||
{
|
||||
actionTypeId: ResponseActionTypesEnum['.endpoint'],
|
||||
params: {
|
||||
command: 'isolate',
|
||||
comment: 'test process comment',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const response = await scheduleNotificationResponseActions({
|
||||
signals,
|
||||
signalsCount: 0,
|
||||
responseActions,
|
||||
});
|
||||
|
||||
expect(response).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,13 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { expandDottedObject } from '../../../../common/utils/expand_dotted';
|
||||
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
|
||||
import type { SetupPlugins } from '../../../plugin_contract';
|
||||
import { ResponseActionTypesEnum } from '../../../../common/api/detection_engine/model/rule_response_actions';
|
||||
import { osqueryResponseAction } from './osquery_response_action';
|
||||
import { endpointResponseAction } from './endpoint_response_action';
|
||||
import type { ScheduleNotificationActions } from '../rule_types/types';
|
||||
import type { AlertWithAgent, Alert } from './types';
|
||||
import type { Alert, AlertWithAgent } from './types';
|
||||
|
||||
interface ScheduleNotificationResponseActionsService {
|
||||
endpointAppContextService: EndpointAppContextService;
|
||||
|
@ -23,10 +24,15 @@ export const getScheduleNotificationResponseActionsService =
|
|||
osqueryCreateActionService,
|
||||
endpointAppContextService,
|
||||
}: ScheduleNotificationResponseActionsService) =>
|
||||
async ({ signals, responseActions }: ScheduleNotificationActions) => {
|
||||
const alerts = (signals as Alert[]).filter((alert) => alert.agent?.id) as AlertWithAgent[];
|
||||
async ({ signals, signalsCount, responseActions }: ScheduleNotificationActions) => {
|
||||
if (!signalsCount || !responseActions?.length) {
|
||||
return;
|
||||
}
|
||||
// expandDottedObject is needed eg in ESQL rule because it's alerts come without nested agent, host etc data but everything is dotted
|
||||
const nestedAlerts = signals.map((signal) => expandDottedObject(signal as object)) as Alert[];
|
||||
const alerts = nestedAlerts.filter((alert) => alert.agent?.id) as AlertWithAgent[];
|
||||
|
||||
await Promise.all(
|
||||
return Promise.all(
|
||||
responseActions.map(async (responseAction) => {
|
||||
if (
|
||||
responseAction.actionTypeId === ResponseActionTypesEnum['.osquery'] &&
|
||||
|
|
|
@ -19,6 +19,14 @@ export type Alert = ParsedTechnicalFields & {
|
|||
process?: {
|
||||
pid: string;
|
||||
};
|
||||
kibana: {
|
||||
alert?: {
|
||||
rule: {
|
||||
uuid: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export interface AlertAgent {
|
||||
|
|
|
@ -162,6 +162,7 @@ export const EqlSpecificRuleParams = z.object({
|
|||
timestampField: TimestampField.optional(),
|
||||
tiebreakerField: TiebreakerField.optional(),
|
||||
alertSuppression: AlertSuppressionCamel.optional(),
|
||||
responseActions: z.array(RuleResponseAction).optional(),
|
||||
});
|
||||
|
||||
export type EqlRuleParams = BaseRuleParams & EqlSpecificRuleParams;
|
||||
|
@ -173,6 +174,7 @@ export const EsqlSpecificRuleParams = z.object({
|
|||
language: z.literal('esql'),
|
||||
query: RuleQuery,
|
||||
alertSuppression: AlertSuppressionCamel.optional(),
|
||||
responseActions: z.array(RuleResponseAction).optional(),
|
||||
});
|
||||
|
||||
export type EsqlRuleParams = BaseRuleParams & EsqlSpecificRuleParams;
|
||||
|
@ -280,6 +282,7 @@ export const NewTermsSpecificRuleParams = z.object({
|
|||
language: KqlQueryLanguage,
|
||||
dataViewId: DataViewId.optional(),
|
||||
alertSuppression: AlertSuppressionCamel.optional(),
|
||||
responseActions: z.array(RuleResponseAction).optional(),
|
||||
});
|
||||
|
||||
export type NewTermsRuleParams = BaseRuleParams & NewTermsSpecificRuleParams;
|
||||
|
|
|
@ -11,16 +11,22 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
|
|||
import { SERVER_APP_ID } from '../../../../../common/constants';
|
||||
import { EqlRuleParams } from '../../rule_schema';
|
||||
import { eqlExecutor } from './eql';
|
||||
import type { CreateRuleOptions, SecurityAlertType, SignalSourceHit } from '../types';
|
||||
import type {
|
||||
CreateRuleOptions,
|
||||
SecurityAlertType,
|
||||
SignalSourceHit,
|
||||
CreateRuleAdditionalOptions,
|
||||
} from '../types';
|
||||
import { validateIndexPatterns } from '../utils';
|
||||
import type { BuildReasonMessage } from '../utils/reason_formatters';
|
||||
import { wrapSuppressedAlerts } from '../utils/wrap_suppressed_alerts';
|
||||
import { getIsAlertSuppressionActive } from '../utils/get_is_alert_suppression_active';
|
||||
|
||||
export const createEqlAlertType = (
|
||||
createOptions: CreateRuleOptions
|
||||
createOptions: CreateRuleOptions & CreateRuleAdditionalOptions
|
||||
): SecurityAlertType<EqlRuleParams, {}, {}, 'default'> => {
|
||||
const { experimentalFeatures, version, licensing } = createOptions;
|
||||
const { experimentalFeatures, version, licensing, scheduleNotificationResponseActionsService } =
|
||||
createOptions;
|
||||
return {
|
||||
id: EQL_RULE_TYPE_ID,
|
||||
name: 'Event Correlation Rule',
|
||||
|
@ -125,6 +131,7 @@ export const createEqlAlertType = (
|
|||
alertWithSuppression,
|
||||
isAlertSuppressionActive: isNonSeqAlertSuppressionActive,
|
||||
experimentalFeatures,
|
||||
scheduleNotificationResponseActionsService,
|
||||
});
|
||||
return { ...result, state };
|
||||
},
|
||||
|
|
|
@ -37,6 +37,7 @@ describe('eql_executor', () => {
|
|||
maxSignals: params.maxSignals,
|
||||
};
|
||||
const mockExperimentalFeatures = {} as ExperimentalFeatures;
|
||||
const mockScheduleNotificationResponseActionsService = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -72,6 +73,8 @@ describe('eql_executor', () => {
|
|||
alertWithSuppression: jest.fn(),
|
||||
isAlertSuppressionActive: false,
|
||||
experimentalFeatures: mockExperimentalFeatures,
|
||||
scheduleNotificationResponseActionsService:
|
||||
mockScheduleNotificationResponseActionsService,
|
||||
});
|
||||
expect(result.warningMessages).toEqual([
|
||||
`The following exceptions won't be applied to rule execution: ${
|
||||
|
@ -121,6 +124,8 @@ describe('eql_executor', () => {
|
|||
alertWithSuppression: jest.fn(),
|
||||
isAlertSuppressionActive: true,
|
||||
experimentalFeatures: mockExperimentalFeatures,
|
||||
scheduleNotificationResponseActionsService:
|
||||
mockScheduleNotificationResponseActionsService,
|
||||
});
|
||||
|
||||
expect(result.warningMessages).toContain(
|
||||
|
@ -154,10 +159,40 @@ describe('eql_executor', () => {
|
|||
alertWithSuppression: jest.fn(),
|
||||
isAlertSuppressionActive: true,
|
||||
experimentalFeatures: mockExperimentalFeatures,
|
||||
scheduleNotificationResponseActionsService: mockScheduleNotificationResponseActionsService,
|
||||
});
|
||||
expect(result.userError).toEqual(true);
|
||||
});
|
||||
|
||||
it('should handle scheduleNotificationResponseActionsService call', async () => {
|
||||
const result = await eqlExecutor({
|
||||
inputIndex: DEFAULT_INDEX_PATTERN,
|
||||
runtimeMappings: {},
|
||||
completeRule: eqlCompleteRule,
|
||||
tuple,
|
||||
ruleExecutionLogger,
|
||||
services: alertServices,
|
||||
version,
|
||||
bulkCreate: jest.fn(),
|
||||
wrapHits: jest.fn(),
|
||||
wrapSequences: jest.fn(),
|
||||
primaryTimestamp: '@timestamp',
|
||||
exceptionFilter: undefined,
|
||||
unprocessedExceptions: [],
|
||||
wrapSuppressedHits: jest.fn(),
|
||||
alertTimestampOverride: undefined,
|
||||
alertWithSuppression: jest.fn(),
|
||||
isAlertSuppressionActive: false,
|
||||
experimentalFeatures: mockExperimentalFeatures,
|
||||
scheduleNotificationResponseActionsService: mockScheduleNotificationResponseActionsService,
|
||||
});
|
||||
expect(mockScheduleNotificationResponseActionsService).toBeCalledWith({
|
||||
signals: result.createdSignals,
|
||||
signalsCount: result.createdSignalsCount,
|
||||
responseActions: eqlCompleteRule.ruleParams.responseActions,
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass frozen tier filters in eql search request', async () => {
|
||||
getDataTierFilterMock.mockResolvedValue([
|
||||
{
|
||||
|
@ -189,6 +224,7 @@ describe('eql_executor', () => {
|
|||
alertWithSuppression: jest.fn(),
|
||||
isAlertSuppressionActive: true,
|
||||
experimentalFeatures: mockExperimentalFeatures,
|
||||
scheduleNotificationResponseActionsService: mockScheduleNotificationResponseActionsService,
|
||||
});
|
||||
|
||||
const searchArgs =
|
||||
|
|
|
@ -26,6 +26,7 @@ import type {
|
|||
SearchAfterAndBulkCreateReturnType,
|
||||
SignalSource,
|
||||
WrapSuppressedHits,
|
||||
CreateRuleAdditionalOptions,
|
||||
} from '../types';
|
||||
import {
|
||||
addToSearchAfterReturn,
|
||||
|
@ -66,6 +67,7 @@ interface EqlExecutorParams {
|
|||
alertWithSuppression: SuppressedAlertService;
|
||||
isAlertSuppressionActive: boolean;
|
||||
experimentalFeatures: ExperimentalFeatures;
|
||||
scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService'];
|
||||
}
|
||||
|
||||
export const eqlExecutor = async ({
|
||||
|
@ -88,6 +90,7 @@ export const eqlExecutor = async ({
|
|||
alertWithSuppression,
|
||||
isAlertSuppressionActive,
|
||||
experimentalFeatures,
|
||||
scheduleNotificationResponseActionsService,
|
||||
}: EqlExecutorParams): Promise<SearchAfterAndBulkCreateReturnType> => {
|
||||
const ruleParams = completeRule.ruleParams;
|
||||
|
||||
|
@ -188,6 +191,14 @@ export const eqlExecutor = async ({
|
|||
result.warningMessages.push(maxSignalsWarning);
|
||||
}
|
||||
|
||||
if (scheduleNotificationResponseActionsService) {
|
||||
scheduleNotificationResponseActionsService({
|
||||
signals: result.createdSignals,
|
||||
signalsCount: result.createdSignalsCount,
|
||||
responseActions: completeRule.ruleParams.responseActions,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (
|
||||
|
|
|
@ -11,12 +11,13 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
|
|||
import { SERVER_APP_ID } from '../../../../../common/constants';
|
||||
import { EsqlRuleParams } from '../../rule_schema';
|
||||
import { esqlExecutor } from './esql';
|
||||
import type { CreateRuleOptions, SecurityAlertType } from '../types';
|
||||
import type { CreateRuleOptions, SecurityAlertType, CreateRuleAdditionalOptions } from '../types';
|
||||
|
||||
export const createEsqlAlertType = (
|
||||
createOptions: CreateRuleOptions
|
||||
createOptions: CreateRuleOptions & CreateRuleAdditionalOptions
|
||||
): SecurityAlertType<EsqlRuleParams, {}, {}, 'default'> => {
|
||||
const { version, experimentalFeatures, licensing } = createOptions;
|
||||
const { version, experimentalFeatures, licensing, scheduleNotificationResponseActionsService } =
|
||||
createOptions;
|
||||
return {
|
||||
id: ESQL_RULE_TYPE_ID,
|
||||
name: 'ES|QL Rule',
|
||||
|
@ -44,6 +45,13 @@ export const createEsqlAlertType = (
|
|||
isExportable: false,
|
||||
category: DEFAULT_APP_CATEGORIES.security.id,
|
||||
producer: SERVER_APP_ID,
|
||||
executor: (params) => esqlExecutor({ ...params, experimentalFeatures, version, licensing }),
|
||||
executor: (params) =>
|
||||
esqlExecutor({
|
||||
...params,
|
||||
experimentalFeatures,
|
||||
version,
|
||||
licensing,
|
||||
scheduleNotificationResponseActionsService,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -28,8 +28,7 @@ import { rowToDocument } from './utils';
|
|||
import { fetchSourceDocuments } from './fetch_source_documents';
|
||||
import { buildReasonMessageForEsqlAlert } from '../utils/reason_formatters';
|
||||
|
||||
import type { RunOpts, SignalSource } from '../types';
|
||||
|
||||
import type { RunOpts, SignalSource, CreateRuleAdditionalOptions } from '../types';
|
||||
import {
|
||||
addToSearchAfterReturn,
|
||||
createSearchAfterReturnType,
|
||||
|
@ -63,6 +62,7 @@ export const esqlExecutor = async ({
|
|||
spaceId,
|
||||
experimentalFeatures,
|
||||
licensing,
|
||||
scheduleNotificationResponseActionsService,
|
||||
}: {
|
||||
runOpts: RunOpts<EsqlRuleParams>;
|
||||
services: RuleExecutorServices<AlertInstanceState, AlertInstanceContext, 'default'>;
|
||||
|
@ -71,6 +71,7 @@ export const esqlExecutor = async ({
|
|||
version: string;
|
||||
experimentalFeatures: ExperimentalFeatures;
|
||||
licensing: LicensingPluginSetup;
|
||||
scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService'];
|
||||
}) => {
|
||||
const ruleParams = completeRule.ruleParams;
|
||||
/**
|
||||
|
@ -225,6 +226,13 @@ export const esqlExecutor = async ({
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (scheduleNotificationResponseActionsService) {
|
||||
scheduleNotificationResponseActionsService({
|
||||
signals: result.createdSignals,
|
||||
signalsCount: result.createdSignalsCount,
|
||||
responseActions: completeRule.ruleParams.responseActions,
|
||||
});
|
||||
}
|
||||
|
||||
// no more results will be found
|
||||
if (response.values.length < size) {
|
||||
|
|
|
@ -12,7 +12,7 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
|
|||
import { SERVER_APP_ID } from '../../../../../common/constants';
|
||||
|
||||
import { NewTermsRuleParams } from '../../rule_schema';
|
||||
import type { CreateRuleOptions, SecurityAlertType } from '../types';
|
||||
import type { CreateRuleOptions, SecurityAlertType, CreateRuleAdditionalOptions } from '../types';
|
||||
import { singleSearchAfter } from '../utils/single_search_after';
|
||||
import { getFilter } from '../utils/get_filter';
|
||||
import { wrapNewTermsAlerts } from './wrap_new_terms_alerts';
|
||||
|
@ -46,9 +46,10 @@ import { multiTermsComposite } from './multi_terms_composite';
|
|||
import type { GenericBulkCreateResponse } from '../utils/bulk_create_with_suppression';
|
||||
|
||||
export const createNewTermsAlertType = (
|
||||
createOptions: CreateRuleOptions
|
||||
createOptions: CreateRuleOptions & CreateRuleAdditionalOptions
|
||||
): SecurityAlertType<NewTermsRuleParams, {}, {}, 'default'> => {
|
||||
const { logger, licensing, experimentalFeatures } = createOptions;
|
||||
const { logger, licensing, experimentalFeatures, scheduleNotificationResponseActionsService } =
|
||||
createOptions;
|
||||
return {
|
||||
id: NEW_TERMS_RULE_TYPE_ID,
|
||||
name: 'New Terms Rule',
|
||||
|
@ -414,6 +415,15 @@ export const createNewTermsAlertType = (
|
|||
|
||||
afterKey = searchResultWithAggs.aggregations.new_terms.after_key;
|
||||
}
|
||||
|
||||
if (scheduleNotificationResponseActionsService) {
|
||||
scheduleNotificationResponseActionsService({
|
||||
signals: result.createdSignals,
|
||||
signalsCount: result.createdSignalsCount,
|
||||
responseActions: completeRule.ruleParams.responseActions,
|
||||
});
|
||||
}
|
||||
|
||||
return { ...result, state };
|
||||
},
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@ import type { UnifiedQueryRuleParams } from '../../rule_schema';
|
|||
import type { ExperimentalFeatures } from '../../../../../common/experimental_features';
|
||||
import { buildReasonMessageForQueryAlert } from '../utils/reason_formatters';
|
||||
import { withSecuritySpan } from '../../../../utils/with_security_span';
|
||||
import type { CreateQueryRuleAdditionalOptions, RunOpts } from '../types';
|
||||
import type { CreateRuleAdditionalOptions, RunOpts } from '../types';
|
||||
|
||||
export const queryExecutor = async ({
|
||||
runOpts,
|
||||
|
@ -42,7 +42,7 @@ export const queryExecutor = async ({
|
|||
version: string;
|
||||
spaceId: string;
|
||||
bucketHistory?: BucketHistory[];
|
||||
scheduleNotificationResponseActionsService?: CreateQueryRuleAdditionalOptions['scheduleNotificationResponseActionsService'];
|
||||
scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService'];
|
||||
licensing: LicensingPluginSetup;
|
||||
}) => {
|
||||
const completeRule = runOpts.completeRule;
|
||||
|
@ -99,13 +99,10 @@ export const queryExecutor = async ({
|
|||
state: {},
|
||||
};
|
||||
|
||||
if (
|
||||
completeRule.ruleParams.responseActions?.length &&
|
||||
result.createdSignalsCount &&
|
||||
scheduleNotificationResponseActionsService
|
||||
) {
|
||||
if (scheduleNotificationResponseActionsService) {
|
||||
scheduleNotificationResponseActionsService({
|
||||
signals: result.createdSignals,
|
||||
signalsCount: result.createdSignalsCount,
|
||||
responseActions: completeRule.ruleParams.responseActions,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -161,15 +161,15 @@ export interface CreateRuleOptions {
|
|||
|
||||
export interface ScheduleNotificationActions {
|
||||
signals: unknown[];
|
||||
responseActions: RuleResponseAction[];
|
||||
signalsCount: number;
|
||||
responseActions: RuleResponseAction[] | undefined;
|
||||
}
|
||||
export interface CreateQueryRuleAdditionalOptions {
|
||||
|
||||
export interface CreateRuleAdditionalOptions {
|
||||
scheduleNotificationResponseActionsService?: (params: ScheduleNotificationActions) => void;
|
||||
}
|
||||
|
||||
export interface CreateQueryRuleOptions
|
||||
extends CreateRuleOptions,
|
||||
CreateQueryRuleAdditionalOptions {
|
||||
export interface CreateQueryRuleOptions extends CreateRuleOptions, CreateRuleAdditionalOptions {
|
||||
id: typeof QUERY_RULE_TYPE_ID | typeof SAVED_QUERY_RULE_TYPE_ID;
|
||||
name: 'Custom Query Rule' | 'Saved Query Rule';
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ import type { IRuleMonitoringService } from './lib/detection_engine/rule_monitor
|
|||
import { createRuleMonitoringService } from './lib/detection_engine/rule_monitoring';
|
||||
import { EndpointMetadataService } from './endpoint/services/metadata';
|
||||
import type {
|
||||
CreateQueryRuleAdditionalOptions,
|
||||
CreateRuleAdditionalOptions,
|
||||
CreateRuleOptions,
|
||||
} from './lib/detection_engine/rule_types/types';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
|
@ -311,7 +311,7 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
analytics: core.analytics,
|
||||
};
|
||||
|
||||
const queryRuleAdditionalOptions: CreateQueryRuleAdditionalOptions = {
|
||||
const ruleAdditionalOptions: CreateRuleAdditionalOptions = {
|
||||
scheduleNotificationResponseActionsService: getScheduleNotificationResponseActionsService({
|
||||
endpointAppContextService: this.endpointAppContextService,
|
||||
osqueryCreateActionService: plugins.osquery.createActionService,
|
||||
|
@ -320,15 +320,19 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
|
||||
const securityRuleTypeWrapper = createSecurityRuleTypeWrapper(securityRuleTypeOptions);
|
||||
|
||||
plugins.alerting.registerType(securityRuleTypeWrapper(createEqlAlertType(ruleOptions)));
|
||||
plugins.alerting.registerType(
|
||||
securityRuleTypeWrapper(createEqlAlertType({ ...ruleOptions, ...ruleAdditionalOptions }))
|
||||
);
|
||||
if (!experimentalFeatures.esqlRulesDisabled) {
|
||||
plugins.alerting.registerType(securityRuleTypeWrapper(createEsqlAlertType(ruleOptions)));
|
||||
plugins.alerting.registerType(
|
||||
securityRuleTypeWrapper(createEsqlAlertType({ ...ruleOptions, ...ruleAdditionalOptions }))
|
||||
);
|
||||
}
|
||||
plugins.alerting.registerType(
|
||||
securityRuleTypeWrapper(
|
||||
createQueryAlertType({
|
||||
...ruleOptions,
|
||||
...queryRuleAdditionalOptions,
|
||||
...ruleAdditionalOptions,
|
||||
id: SAVED_QUERY_RULE_TYPE_ID,
|
||||
name: 'Saved Query Rule',
|
||||
})
|
||||
|
@ -342,14 +346,16 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
securityRuleTypeWrapper(
|
||||
createQueryAlertType({
|
||||
...ruleOptions,
|
||||
...queryRuleAdditionalOptions,
|
||||
...ruleAdditionalOptions,
|
||||
id: QUERY_RULE_TYPE_ID,
|
||||
name: 'Custom Query Rule',
|
||||
})
|
||||
)
|
||||
);
|
||||
plugins.alerting.registerType(securityRuleTypeWrapper(createThresholdAlertType(ruleOptions)));
|
||||
plugins.alerting.registerType(securityRuleTypeWrapper(createNewTermsAlertType(ruleOptions)));
|
||||
plugins.alerting.registerType(
|
||||
securityRuleTypeWrapper(createNewTermsAlertType({ ...ruleOptions, ...ruleAdditionalOptions }))
|
||||
);
|
||||
|
||||
// TODO We need to get the endpoint routes inside of initRoutes
|
||||
initRoutes(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue