From 751d300600966c5cb0e4b098365f4964105797e5 Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Mon, 27 Jun 2022 09:42:02 -0700 Subject: [PATCH] Replace patch, patch bulk, import rules schemas --- .../request/patch_rules_bulk_schema.ts | 4 +- .../schemas/request/rule_schemas.ts | 89 +++++- .../rules/add_prepackaged_rules_route.ts | 5 +- .../routes/rules/import_rules_route.ts | 8 +- .../routes/rules/patch_rules_bulk_route.ts | 131 +------- .../routes/rules/patch_rules_route.ts | 136 +-------- .../detection_engine/routes/rules/utils.ts | 24 +- .../utils/check_rule_exception_references.ts | 7 +- .../utils/gather_referenced_exceptions.ts | 4 +- .../routes/rules/utils/import_rules_utils.ts | 208 ++----------- .../rules/create_rules_stream_from_ndjson.ts | 15 +- .../rules/get_prepackaged_rules.ts | 25 +- .../rules/get_rules_to_install.ts | 4 +- .../rules/get_rules_to_update.ts | 10 +- .../rules/install_prepacked_rules.ts | 134 +-------- .../lib/detection_engine/rules/patch_rules.ts | 234 ++------------- .../lib/detection_engine/rules/types.ts | 84 +----- .../rules/update_prepacked_rules.ts | 212 ++----------- .../detection_engine/rules/update_rules.ts | 20 +- .../schemas/rule_converters.ts | 280 +++++++++++++++++- .../detection_engine/schemas/rule_schemas.ts | 6 + 21 files changed, 533 insertions(+), 1107 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_bulk_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_bulk_schema.ts index 3b6919710c3fc..cb807a7a8eaa7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_bulk_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_bulk_schema.ts @@ -7,9 +7,7 @@ import * as t from 'io-ts'; -import { patchRulesSchema, PatchRulesSchemaDecoded } from './patch_rules_schema'; +import { patchRulesSchema } from './rule_schemas'; export const patchRulesBulkSchema = t.array(patchRulesSchema); export type PatchRulesBulkSchema = t.TypeOf; - -export type PatchRulesBulkSchemaDecoded = PatchRulesSchemaDecoded[]; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index 33127dc434d82..91214d009207d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -27,7 +27,7 @@ import { throttle, } from '@kbn/securitysolution-io-ts-alerting-types'; import { listArray } from '@kbn/securitysolution-io-ts-list-types'; -import { version } from '@kbn/securitysolution-io-ts-types'; +import { OnlyFalseAllowed, version } from '@kbn/securitysolution-io-ts-types'; import { id, @@ -207,6 +207,14 @@ export const sharedUpdateSchema = t.intersection([ ]); export type SharedUpdateSchema = t.TypeOf; +export const sharedPatchSchema = t.intersection([ + basePatchParams, + t.exact(t.partial({ rule_id })), + t.exact(t.partial({ id })), +]); + +// START type specific parameter definitions +// ----------------------------------------- const eqlRuleParams = { required: { type: t.literal('eql'), @@ -348,6 +356,8 @@ const { } = buildAPISchemas(machineLearningRuleParams); export { machineLearningCreateParams }; +// --------------------------------------- +// END type specific parameter definitions const createTypeSpecific = t.union([ eqlCreateParams, @@ -379,6 +389,43 @@ export const previewRulesSchema = t.intersection([ ]); export type PreviewRulesSchema = t.TypeOf; +// TODO: test `version` overwriting `version` from baseParams +export const addPrepackagedRulesSchema = t.intersection([ + baseCreateParams, + createTypeSpecific, + // version is required in addPrepackagedRulesSchema, so this supercedes the defaultable + // version in baseParams + t.exact(t.type({ rule_id, version })), + t.exact( + t.partial({ + related_integrations: RelatedIntegrationArray, + required_fields: RequiredFieldArray, + setup: SetupGuide, + }) + ), +]); +export type AddPrepackagedRulesSchema = t.TypeOf; + +export const importRulesSchema = t.intersection([ + baseCreateParams, + createTypeSpecific, + t.exact(t.type({ rule_id })), + t.exact( + t.partial({ + id, + immutable: OnlyFalseAllowed, + updated_at, + updated_by, + created_at, + created_by, + related_integrations: RelatedIntegrationArray, + required_fields: RequiredFieldArray, + setup: SetupGuide, + }) + ), +]); +export type ImportRulesSchema = t.TypeOf; + type UpdateSchema = SharedUpdateSchema & T; export type EqlUpdateSchema = UpdateSchema>; export type ThreatMatchUpdateSchema = UpdateSchema>; @@ -397,6 +444,22 @@ const patchTypeSpecific = t.union([ thresholdPatchParams, machineLearningPatchParams, ]); +export { + eqlPatchParams, + threatMatchPatchParams, + queryPatchParams, + savedQueryPatchParams, + thresholdPatchParams, + machineLearningPatchParams, +}; +export type PatchTypeSpecific = t.TypeOf; + +export type EqlPatchParams = t.TypeOf; +export type ThreatMatchPatchParams = t.TypeOf; +export type QueryPatchParams = t.TypeOf; +export type SavedQueryPatchParams = t.TypeOf; +export type ThresholdPatchParams = t.TypeOf; +export type MachineLearningPatchParams = t.TypeOf; const responseTypeSpecific = t.union([ eqlResponseParams, @@ -411,11 +474,27 @@ export type ResponseTypeSpecific = t.TypeOf; export const updateRulesSchema = t.intersection([createTypeSpecific, sharedUpdateSchema]); export type UpdateRulesSchema = t.TypeOf; -export const fullPatchSchema = t.intersection([ - basePatchParams, - patchTypeSpecific, - t.exact(t.partial({ id })), +export const eqlFullPatchSchema = t.intersection([eqlPatchParams, sharedPatchSchema]); +export type EqlFullPatchSchema = t.TypeOf; +export const threatMatchFullPatchSchema = t.intersection([ + threatMatchPatchParams, + sharedPatchSchema, ]); +export type ThreatMatchFullPatchSchema = t.TypeOf; +export const queryFullPatchSchema = t.intersection([queryPatchParams, sharedPatchSchema]); +export type QueryFullPatchSchema = t.TypeOf; +export const savedQueryFullPatchSchema = t.intersection([savedQueryPatchParams, sharedPatchSchema]); +export type SavedQueryFullPatchSchema = t.TypeOf; +export const thresholdFullPatchSchema = t.intersection([thresholdPatchParams, sharedPatchSchema]); +export type ThresholdFullPatchSchema = t.TypeOf; +export const machineLearningFullPatchSchema = t.intersection([ + machineLearningPatchParams, + sharedPatchSchema, +]); +export type MachineLearningFullPatchSchema = t.TypeOf; + +export const patchRulesSchema = t.intersection([patchTypeSpecific, sharedPatchSchema]); +export type PatchRulesSchema = t.TypeOf; const responseRequiredFields = { id, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 145558cd4a104..7a9d30b2ec8cf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -118,9 +118,8 @@ export const createPrepackagedRules = async ( }); const rulesToInstall = getRulesToInstall(latestPrepackagedRules, prepackagedRules); const rulesToUpdate = getRulesToUpdate(latestPrepackagedRules, prepackagedRules); - const signalsIndex = siemClient.getSignalsIndex(); - await Promise.all(installPrepackagedRules(rulesClient, rulesToInstall, signalsIndex)); + await Promise.all(installPrepackagedRules(rulesClient, rulesToInstall, siemClient)); const timeline = await installPrepackagedTimelines( maxTimelineImportExportSize, frameworkRequest, @@ -134,7 +133,7 @@ export const createPrepackagedRules = async ( rulesClient, savedObjectsClient, rulesToUpdate, - signalsIndex, + siemClient, context.getRuleExecutionLog() ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index 00d10ef8be9fa..38a9bb622768d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -45,7 +45,7 @@ import { } from './utils/import_rules_utils'; import { getReferencedExceptionLists } from './utils/gather_referenced_exceptions'; import { importRuleExceptions } from './utils/import_rule_exceptions'; -import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request'; +import { ImportRulesSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; const CHUNK_PARSED_OBJECT_SIZE = 50; @@ -91,6 +91,7 @@ export const importRulesRoute = ( }); const savedObjectsClient = ctx.core.savedObjects.client; const exceptionsClient = ctx.lists?.getExceptionListClient(); + const siemClient = ctx.securitySolution.getAppClient(); const mlAuthz = buildMlAuthz({ license: ctx.licensing.license, @@ -140,10 +141,10 @@ export const importRulesRoute = ( let parsedRules; let actionErrors: BulkError[] = []; const actualRules = rules.filter( - (rule): rule is ImportRulesSchemaDecoded => !(rule instanceof Error) + (rule): rule is ImportRulesSchema => !(rule instanceof Error) ); - if (actualRules.some((rule) => rule.actions.length > 0)) { + if (actualRules.some((rule) => rule.actions && rule.actions.length > 0)) { const [nonExistentActionErrors, uniqueParsedObjects] = await getInvalidConnectors( migratedParsedObjectsWithoutDuplicateErrors, actionsClient @@ -171,6 +172,7 @@ export const importRulesRoute = ( exceptionsClient, spaceId: ctx.securitySolution.getSpaceId(), existingLists: foundReferencedExceptionLists, + siemClient, }); const errorsResp = importRuleResponse.filter((resp) => isBulkError(resp)) as BulkError[]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 8cb03c7790733..5aae18f6e0df0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -7,11 +7,7 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import { Logger } from '@kbn/core/server'; -import { RuleAlertAction } from '../../../../../common/detection_engine/types'; -import { - patchRulesBulkSchema, - PatchRulesBulkSchemaDecoded, -} from '../../../../../common/detection_engine/schemas/request/patch_rules_bulk_schema'; +import { patchRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/patch_rules_bulk_schema'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; import type { SecuritySolutionPluginRouter } from '../../../../types'; @@ -24,7 +20,6 @@ import { getIdBulkError } from './utils'; import { transformValidateBulkError } from './validate'; import { patchRules } from '../../rules/patch_rules'; import { readRules } from '../../rules/read_rules'; -import { PartialFilter } from '../../types'; import { legacyMigrate } from '../../rules/utils'; import { getDeprecatedBulkEndpointHeader, logDeprecatedBulkEndpoint } from './utils/deprecation'; @@ -40,9 +35,7 @@ export const patchRulesBulkRoute = ( { path: DETECTION_ENGINE_RULES_BULK_UPDATE, validate: { - body: buildRouteValidation( - patchRulesBulkSchema - ), + body: buildRouteValidation(patchRulesBulkSchema), }, options: { tags: ['access:securitySolution'], @@ -67,75 +60,18 @@ export const patchRulesBulkRoute = ( }); const rules = await Promise.all( request.body.map(async (payloadRule) => { - const { - actions: actionsRest, - author, - building_block_type: buildingBlockType, - description, - enabled, - timestamp_field: timestampField, - event_category_override: eventCategoryOverride, - tiebreaker_field: tiebreakerField, - false_positives: falsePositives, - from, - query, - language, - license, - output_index: outputIndex, - saved_id: savedId, - timeline_id: timelineId, - timeline_title: timelineTitle, - meta, - filters: filtersRest, - rule_id: ruleId, - id, - index, - data_view_id: dataViewId, - interval, - max_signals: maxSignals, - risk_score: riskScore, - risk_score_mapping: riskScoreMapping, - rule_name_override: ruleNameOverride, - name, - severity, - severity_mapping: severityMapping, - tags, - to, - type, - threat, - threshold, - threat_filters: threatFilters, - threat_index: threatIndex, - threat_indicator_path: threatIndicatorPath, - threat_query: threatQuery, - threat_mapping: threatMapping, - threat_language: threatLanguage, - concurrent_searches: concurrentSearches, - items_per_search: itemsPerSearch, - timestamp_override: timestampOverride, - throttle, - references, - note, - version, - anomaly_threshold: anomalyThreshold, - machine_learning_job_id: machineLearningJobId, - exceptions_list: exceptionsList, - } = payloadRule; - const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; - // TODO: Fix these either with an is conversion or by better typing them within io-ts - const actions: RuleAlertAction[] = actionsRest as RuleAlertAction[]; - const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; + const idOrRuleIdOrUnknown = payloadRule.id ?? payloadRule.rule_id ?? '(unknown id)'; try { - if (type) { + if (payloadRule.type) { // reject an unauthorized "promotion" to ML - throwAuthzError(await mlAuthz.validateRuleType(type)); + throwAuthzError(await mlAuthz.validateRuleType(payloadRule.type)); } const existingRule = await readRules({ rulesClient, - ruleId, - id, + ruleId: payloadRule.rule_id, + id: payloadRule.id, }); if (existingRule?.params.type) { // reject an unauthorized modification of an ML rule @@ -151,62 +87,13 @@ export const patchRulesBulkRoute = ( const rule = await patchRules({ rule: migratedRule, rulesClient, - author, - buildingBlockType, - description, - enabled, - timestampField, - eventCategoryOverride, - tiebreakerField, - falsePositives, - from, - query, - language, - license, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - index, - dataViewId, - interval, - maxSignals, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - severity, - severityMapping, - tags, - to, - type, - threat, - threshold, - threatFilters, - threatIndex, - threatIndicatorPath, - threatQuery, - threatMapping, - threatLanguage, - throttle, - concurrentSearches, - itemsPerSearch, - timestampOverride, - references, - note, - version, - anomalyThreshold, - machineLearningJobId, - actions, - exceptionsList, + params: payloadRule, }); if (rule != null && rule.enabled != null && rule.name != null) { const ruleExecutionSummary = await ruleExecutionLog.getExecutionSummary(rule.id); return transformValidateBulkError(rule.id, rule, ruleExecutionSummary); } else { - return getIdBulkError({ id, ruleId }); + return getIdBulkError({ id: payloadRule.id, ruleId: payloadRule.rule_id }); } } catch (err) { return transformBulkError(idOrRuleIdOrUnknown, err); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts index d704f5678f53d..36078d7d1b4e8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -6,13 +6,8 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; -import { RuleAlertAction } from '../../../../../common/detection_engine/types'; -import { patchRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/patch_rules_type_dependents'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import { - PatchRulesSchemaDecoded, - patchRulesSchema, -} from '../../../../../common/detection_engine/schemas/request/patch_rules_schema'; +import { patchRulesSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; @@ -25,16 +20,13 @@ import { getIdError } from './utils'; import { transformValidate } from './validate'; import { readRules } from '../../rules/read_rules'; import { legacyMigrate } from '../../rules/utils'; -import { PartialFilter } from '../../types'; export const patchRulesRoute = (router: SecuritySolutionPluginRouter, ml: SetupPlugins['ml']) => { router.patch( { path: DETECTION_ENGINE_RULES_URL, validate: { - body: buildRouteValidation( - patchRulesSchema - ), + body: buildRouteValidation(patchRulesSchema), }, options: { tags: ['access:securitySolution'], @@ -42,69 +34,8 @@ export const patchRulesRoute = (router: SecuritySolutionPluginRouter, ml: SetupP }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - const validationErrors = patchRuleValidateTypeDependents(request.body); - if (validationErrors.length) { - return siemResponse.error({ statusCode: 400, body: validationErrors }); - } - const { - actions: actionsRest, - author, - building_block_type: buildingBlockType, - description, - enabled, - timestamp_field: timestampField, - event_category_override: eventCategoryOverride, - tiebreaker_field: tiebreakerField, - false_positives: falsePositives, - from, - query, - language, - license, - output_index: outputIndex, - saved_id: savedId, - timeline_id: timelineId, - timeline_title: timelineTitle, - meta, - filters: filtersRest, - rule_id: ruleId, - id, - index, - data_view_id: dataViewId, - interval, - max_signals: maxSignals, - risk_score: riskScore, - risk_score_mapping: riskScoreMapping, - rule_name_override: ruleNameOverride, - name, - severity, - severity_mapping: severityMapping, - tags, - to, - type, - threat, - threshold, - threat_filters: threatFilters, - threat_index: threatIndex, - threat_indicator_path: threatIndicatorPath, - threat_query: threatQuery, - threat_mapping: threatMapping, - threat_language: threatLanguage, - concurrent_searches: concurrentSearches, - items_per_search: itemsPerSearch, - timestamp_override: timestampOverride, - throttle, - references, - note, - version, - anomaly_threshold: anomalyThreshold, - machine_learning_job_id: machineLearningJobId, - exceptions_list: exceptionsList, - } = request.body; try { - // TODO: Fix these either with an is conversion or by better typing them within io-ts - const actions: RuleAlertAction[] = actionsRest as RuleAlertAction[]; - const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; - + const params = request.body; const rulesClient = (await context.alerting).getRulesClient(); const ruleExecutionLog = (await context.securitySolution).getRuleExecutionLog(); const savedObjectsClient = (await context.core).savedObjects.client; @@ -115,15 +46,15 @@ export const patchRulesRoute = (router: SecuritySolutionPluginRouter, ml: SetupP request, savedObjectsClient, }); - if (type) { + if (params.type) { // reject an unauthorized "promotion" to ML - throwAuthzError(await mlAuthz.validateRuleType(type)); + throwAuthzError(await mlAuthz.validateRuleType(params.type)); } const existingRule = await readRules({ rulesClient, - ruleId, - id, + ruleId: params.rule_id, + id: params.id, }); if (existingRule?.params.type) { // reject an unauthorized modification of an ML rule @@ -138,57 +69,8 @@ export const patchRulesRoute = (router: SecuritySolutionPluginRouter, ml: SetupP const rule = await patchRules({ rulesClient, - author, - buildingBlockType, - description, - enabled, - timestampField, - eventCategoryOverride, - tiebreakerField, - falsePositives, - from, - query, - language, - license, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, rule: migratedRule, - index, - dataViewId, - interval, - maxSignals, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - severity, - severityMapping, - tags, - to, - type, - threat, - threshold, - threatFilters, - threatIndex, - threatIndicatorPath, - threatQuery, - threatMapping, - threatLanguage, - throttle, - concurrentSearches, - itemsPerSearch, - timestampOverride, - references, - note, - version, - anomalyThreshold, - machineLearningJobId, - actions, - exceptionsList, + params, }); if (rule != null && rule.enabled != null && rule.name != null) { const ruleExecutionSummary = await ruleExecutionLog.getExecutionSummary(rule.id); @@ -200,7 +82,7 @@ export const patchRulesRoute = (router: SecuritySolutionPluginRouter, ml: SetupP return response.ok({ body: validated ?? {} }); } } else { - const error = getIdError({ id, ruleId }); + const error = getIdError({ id: params.id, ruleId: params.rule_id }); return siemResponse.error({ body: error.message, statusCode: error.statusCode, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index 850d971b39c59..004d38932ea70 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -15,7 +15,7 @@ import { PartialRule, FindResult } from '@kbn/alerting-plugin/server'; import { ActionsClient, FindActionResult } from '@kbn/actions-plugin/server'; import { RuleExecutionSummary } from '../../../../../common/detection_engine/schemas/common'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; -import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; +import { ImportRulesSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; import { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import { RuleAlertType, isAlertType } from '../../rules/types'; import { createBulkErrorObject, BulkError, OutputError } from '../utils'; @@ -25,7 +25,7 @@ import { RuleParams } from '../../schemas/rule_schemas'; import { LegacyRulesActionsSavedObject } from '../../rule_actions/legacy_get_rule_actions_saved_object'; import { RuleExecutionSummariesByRuleId } from '../../rule_execution_log'; -type PromiseFromStreams = ImportRulesSchemaDecoded | Error; +type PromiseFromStreams = ImportRulesSchema | Error; const MAX_CONCURRENT_SEARCHES = 10; export const getIdError = ({ @@ -245,7 +245,7 @@ export const migrateLegacyActionsIds = async ( rules: PromiseFromStreams[], savedObjectsClient: SavedObjectsClientContract ): Promise => { - const isImportRule = (r: unknown): r is ImportRulesSchemaDecoded => !(r instanceof Error); + const isImportRule = (r: unknown): r is ImportRulesSchema => !(r instanceof Error); const toReturn = await pMap( rules, @@ -254,7 +254,7 @@ export const migrateLegacyActionsIds = async ( // 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 = await pMap( - rule.actions, + rule.actions ?? [], (action: Action) => swapActionIds(action, savedObjectsClient), { concurrency: MAX_CONCURRENT_SEARCHES } ); @@ -332,13 +332,15 @@ export const getInvalidConnectors = async ( acc.rulesAcc.set(uuid.v4(), parsedRule); } else { const { rule_id: ruleId, actions } = parsedRule; - const missingActionIds = actions.flatMap((action) => { - if (!actionIds.has(action.id)) { - return [action.id]; - } else { - return []; - } - }); + const missingActionIds = actions + ? actions.flatMap((action) => { + if (!actionIds.has(action.id)) { + return [action.id]; + } else { + return []; + } + }) + : []; if (missingActionIds.length === 0) { acc.rulesAcc.set(ruleId, parsedRule); } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/check_rule_exception_references.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/check_rule_exception_references.ts index 11600443a9f6b..f068ba04faa8c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/check_rule_exception_references.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/check_rule_exception_references.ts @@ -6,7 +6,7 @@ */ import { ListArray, ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { ImportRulesSchemaDecoded } from '../../../../../../common/detection_engine/schemas/request/import_rules_schema'; +import { ImportRulesSchema } from '../../../../../../common/detection_engine/schemas/request/rule_schemas'; import { BulkError, createBulkErrorObject } from '../../utils'; /** @@ -24,12 +24,13 @@ export const checkRuleExceptionReferences = ({ rule, existingLists, }: { - rule: ImportRulesSchemaDecoded; + rule: ImportRulesSchema; existingLists: Record; }): [BulkError[], ListArray] => { let ruleExceptions: ListArray = []; let errors: BulkError[] = []; - const { exceptions_list: exceptionLists, rule_id: ruleId } = rule; + const { rule_id: ruleId } = rule; + const exceptionLists = rule.exceptions_list ?? []; if (!exceptionLists.length) { return [[], []]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/gather_referenced_exceptions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/gather_referenced_exceptions.ts index 4033e572553f5..df57fc54086c1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/gather_referenced_exceptions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/gather_referenced_exceptions.ts @@ -11,7 +11,7 @@ import { getAllListTypes, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '@kbn/lists-plugin/server/services/exception_lists/utils/import/find_all_exception_list_types'; -import { ImportRulesSchemaDecoded } from '../../../../../../common/detection_engine/schemas/request'; +import { ImportRulesSchema } from '../../../../../../common/detection_engine/schemas/request/rule_schemas'; /** * Helper that takes rules, goes through their referenced exception lists and @@ -24,7 +24,7 @@ export const getReferencedExceptionLists = async ({ rules, savedObjectsClient, }: { - rules: Array; + rules: Array; savedObjectsClient: SavedObjectsClientContract; }): Promise> => { const [lists] = rules.reduce((acc, rule) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts index dd98137b3b2e4..4951854622761 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils/import_rules_utils.ts @@ -15,18 +15,17 @@ import { import { RulesClient } from '@kbn/alerting-plugin/server'; import { ExceptionListClient } from '@kbn/lists-plugin/server'; import { legacyMigrate } from '../../../rules/utils'; -import { PartialFilter } from '../../../types'; import { createBulkErrorObject, ImportRuleResponse } from '../../utils'; -import { isMlRule } from '../../../../../../common/machine_learning/helpers'; -import { createRules } from '../../../rules/create_rules'; import { readRules } from '../../../rules/read_rules'; import { patchRules } from '../../../rules/patch_rules'; -import { ImportRulesSchemaDecoded } from '../../../../../../common/detection_engine/schemas/request/import_rules_schema'; +import { ImportRulesSchema } from '../../../../../../common/detection_engine/schemas/request/rule_schemas'; import { MlAuthz } from '../../../../machine_learning/authz'; import { throwAuthzError } from '../../../../machine_learning/validation'; import { checkRuleExceptionReferences } from './check_rule_exception_references'; +import { convertCreateAPIToInternalSchema } from '../../../schemas/rule_converters'; +import { AppClient } from '../../../../../types'; -export type PromiseFromStreams = ImportRulesSchemaDecoded | Error; +export type PromiseFromStreams = ImportRulesSchema | Error; export interface RuleExceptionsPromiseFromStreams { rules: PromiseFromStreams[]; exceptions: Array; @@ -59,6 +58,7 @@ export const importRules = async ({ exceptionsClient, spaceId, existingLists, + siemClient, }: { ruleChunks: PromiseFromStreams[][]; rulesResponseAcc: ImportRuleResponse[]; @@ -69,6 +69,7 @@ export const importRules = async ({ exceptionsClient: ExceptionListClient | undefined; spaceId: string; existingLists: Record; + siemClient: AppClient; }) => { let importRuleResponse: ImportRuleResponse[] = [...rulesResponseAcc]; @@ -95,63 +96,6 @@ export const importRules = async ({ return null; } - const { - anomaly_threshold: anomalyThreshold, - author, - building_block_type: buildingBlockType, - description, - enabled, - timestamp_field: timestampField, - event_category_override: eventCategoryOverride, - tiebreaker_field: tiebreakerField, - false_positives: falsePositives, - from, - immutable, - query: queryOrUndefined, - language: languageOrUndefined, - license, - machine_learning_job_id: machineLearningJobId, - output_index: outputIndex, - saved_id: savedId, - meta, - filters: filtersRest, - rule_id: ruleId, - index, - data_view_id: dataViewId, - interval, - max_signals: maxSignals, - related_integrations: relatedIntegrations, - required_fields: requiredFields, - risk_score: riskScore, - risk_score_mapping: riskScoreMapping, - rule_name_override: ruleNameOverride, - name, - setup, - severity, - severity_mapping: severityMapping, - tags, - threat, - threat_filters: threatFilters, - threat_index: threatIndex, - threat_query: threatQuery, - threat_mapping: threatMapping, - threat_language: threatLanguage, - threat_indicator_path: threatIndicatorPath, - concurrent_searches: concurrentSearches, - items_per_search: itemsPerSearch, - threshold, - timestamp_override: timestampOverride, - to, - type, - references, - note, - timeline_id: timelineId, - timeline_title: timelineTitle, - throttle, - version, - actions, - } = parsedRule; - try { const [exceptionErrors, exceptions] = checkRuleExceptionReferences({ rule: parsedRule, @@ -160,78 +104,25 @@ export const importRules = async ({ importRuleResponse = [...importRuleResponse, ...exceptionErrors]; - const query = !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined; - const language = - !isMlRule(type) && languageOrUndefined == null ? 'kuery' : languageOrUndefined; // TODO: Fix these either with an is conversion or by better typing them within io-ts - const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; - throwAuthzError(await mlAuthz.validateRuleType(type)); + throwAuthzError(await mlAuthz.validateRuleType(parsedRule.type)); const rule = await readRules({ rulesClient, - ruleId, + ruleId: parsedRule.rule_id, id: undefined, }); if (rule == null) { - await createRules({ - rulesClient, - anomalyThreshold, - author, - buildingBlockType, - description, - enabled, - timestampField, - eventCategoryOverride, - tiebreakerField, - falsePositives, - from, - immutable, - query, - language, - license, - machineLearningJobId, - outputIndex: '', - savedId, - timelineId, - timelineTitle, - meta, - filters, - ruleId, - index, - dataViewId, - interval, - maxSignals, - name, - relatedIntegrations, - requiredFields, - riskScore, - riskScoreMapping, - ruleNameOverride, - setup, - severity, - severityMapping, - tags, - throttle, - to, - type, - threat, - threshold, - threatFilters, - threatIndex, - threatIndicatorPath, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - timestampOverride, - references, - note, - version, - exceptionsList: [...exceptions], - actions, + const internalRule = convertCreateAPIToInternalSchema( + parsedRule, + siemClient, + false + ); + + await rulesClient.create({ + data: internalRule, }); resolve({ - rule_id: ruleId, + rule_id: parsedRule.rule_id, status_code: 200, }); } else if (rule != null && overwriteRules) { @@ -242,78 +133,29 @@ export const importRules = async ({ }); await patchRules({ rulesClient, - author, - buildingBlockType, - description, - enabled, - timestampField, - eventCategoryOverride, - tiebreakerField, - falsePositives, - from, - query, - language, - license, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, rule: migratedRule, - index, - dataViewId, - interval, - maxSignals, - relatedIntegrations, - requiredFields, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - setup, - severity, - severityMapping, - tags, - timestampOverride, - throttle, - to, - type, - threat, - threshold, - threatFilters, - threatIndex, - threatIndicatorPath, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - references, - note, - version, - exceptionsList: [...exceptions], - anomalyThreshold, - machineLearningJobId, - actions, + params: { + ...parsedRule, + exceptions_list: [...exceptions], + }, }); resolve({ - rule_id: ruleId, + rule_id: parsedRule.rule_id, status_code: 200, }); } else if (rule != null) { resolve( createBulkErrorObject({ - ruleId, + ruleId: parsedRule.rule_id, statusCode: 409, - message: `rule_id: "${ruleId}" already exists`, + message: `rule_id: "${parsedRule.rule_id}" already exists`, }) ); } } catch (err) { resolve( createBulkErrorObject({ - ruleId, + ruleId: parsedRule.rule_id, statusCode: err.statusCode ?? 400, message: err.message, }) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts index 1b1508bb079e4..f1e3ea1653f44 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts @@ -27,8 +27,7 @@ import { importRuleValidateTypeDependents } from '../../../../common/detection_e import { importRulesSchema, ImportRulesSchema, - ImportRulesSchemaDecoded, -} from '../../../../common/detection_engine/schemas/request/import_rules_schema'; +} from '../../../../common/detection_engine/schemas/request/rule_schemas'; import { parseNdjsonStrings, createRulesLimitStream, @@ -41,7 +40,7 @@ import { export const validateRulesStream = (): Transform => { return createMapStream<{ exceptions: Array; - rules: Array; + rules: Array; }>((items) => ({ exceptions: items.exceptions, rules: validateRules(items.rules), @@ -50,20 +49,20 @@ export const validateRulesStream = (): Transform => { export const validateRules = ( rules: Array -): Array => { +): Array => { return rules.map((obj: ImportRulesSchema | Error) => { if (!(obj instanceof Error)) { const decoded = importRulesSchema.decode(obj); const checked = exactCheck(obj, decoded); - const onLeft = (errors: t.Errors): BadRequestError | ImportRulesSchemaDecoded => { + const onLeft = (errors: t.Errors): BadRequestError | ImportRulesSchema => { return new BadRequestError(formatErrors(errors).join()); }; - const onRight = (schema: ImportRulesSchema): BadRequestError | ImportRulesSchemaDecoded => { + const onRight = (schema: ImportRulesSchema): BadRequestError | ImportRulesSchema => { const validationErrors = importRuleValidateTypeDependents(schema); if (validationErrors.length) { return new BadRequestError(validationErrors.join()); } else { - return schema as ImportRulesSchemaDecoded; + return schema as ImportRulesSchema; } }; return pipe(checked, fold(onLeft, onRight)); @@ -82,7 +81,7 @@ export const validateRules = ( export const sortImports = (): Transform => { return createReduceStream<{ exceptions: Array; - rules: Array; + rules: Array; }>( (acc, importItem) => { if (has('list_id', importItem) || has('item_id', importItem) || has('entries', importItem)) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts index cc70dd1008de7..b806bc9681c2f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts @@ -14,8 +14,7 @@ import { SavedObjectAttributes } from '@kbn/core/types'; import { addPrepackagedRulesSchema, AddPrepackagedRulesSchema, - AddPrepackagedRulesSchemaDecoded, -} from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; +} from '../../../../common/detection_engine/schemas/request/rule_schemas'; // TODO: convert rules files to TS and add explicit type definitions import { rawRules } from './prepackaged_rules'; @@ -30,12 +29,12 @@ import { ConfigType } from '../../../config'; */ export const validateAllPrepackagedRules = ( rules: AddPrepackagedRulesSchema[] -): AddPrepackagedRulesSchemaDecoded[] => { +): AddPrepackagedRulesSchema[] => { return rules.map((rule) => { const decoded = addPrepackagedRulesSchema.decode(rule); const checked = exactCheck(rule, decoded); - const onLeft = (errors: t.Errors): AddPrepackagedRulesSchemaDecoded => { + const onLeft = (errors: t.Errors): AddPrepackagedRulesSchema => { const ruleName = rule.name ? rule.name : '(rule name unknown)'; const ruleId = rule.rule_id ? rule.rule_id : '(rule rule_id unknown)'; throw new BadRequestError( @@ -48,8 +47,8 @@ export const validateAllPrepackagedRules = ( ); }; - const onRight = (schema: AddPrepackagedRulesSchema): AddPrepackagedRulesSchemaDecoded => { - return schema as AddPrepackagedRulesSchemaDecoded; + const onRight = (schema: AddPrepackagedRulesSchema): AddPrepackagedRulesSchema => { + return schema as AddPrepackagedRulesSchema; }; return pipe(checked, fold(onLeft, onRight)); }); @@ -60,12 +59,12 @@ export const validateAllPrepackagedRules = ( */ export const validateAllRuleSavedObjects = ( rules: Array -): AddPrepackagedRulesSchemaDecoded[] => { +): AddPrepackagedRulesSchema[] => { return rules.map((rule) => { const decoded = addPrepackagedRulesSchema.decode(rule); const checked = exactCheck(rule, decoded); - const onLeft = (errors: t.Errors): AddPrepackagedRulesSchemaDecoded => { + const onLeft = (errors: t.Errors): AddPrepackagedRulesSchema => { const ruleName = rule.name ? rule.name : '(rule name unknown)'; const ruleId = rule.rule_id ? rule.rule_id : '(rule rule_id unknown)'; throw new BadRequestError( @@ -78,8 +77,8 @@ export const validateAllRuleSavedObjects = ( ); }; - const onRight = (schema: AddPrepackagedRulesSchema): AddPrepackagedRulesSchemaDecoded => { - return schema as AddPrepackagedRulesSchemaDecoded; + const onRight = (schema: AddPrepackagedRulesSchema): AddPrepackagedRulesSchema => { + return schema as AddPrepackagedRulesSchema; }; return pipe(checked, fold(onLeft, onRight)); }); @@ -90,7 +89,7 @@ export const validateAllRuleSavedObjects = ( */ export const getFleetInstalledRules = async ( client: RuleAssetSavedObjectsClient -): Promise => { +): Promise => { const fleetResponse = await client.all(); const fleetRules = fleetResponse.map((so) => so.attributes); return validateAllRuleSavedObjects(fleetRules); @@ -99,7 +98,7 @@ export const getFleetInstalledRules = async ( export const getPrepackagedRules = ( // @ts-expect-error mock data is too loosely typed rules: AddPrepackagedRulesSchema[] = rawRules -): AddPrepackagedRulesSchemaDecoded[] => { +): AddPrepackagedRulesSchema[] => { return validateAllPrepackagedRules(rules); }; @@ -107,7 +106,7 @@ export const getLatestPrepackagedRules = async ( client: RuleAssetSavedObjectsClient, prebuiltRulesFromFileSystem: ConfigType['prebuiltRulesFromFileSystem'], prebuiltRulesFromSavedObjects: ConfigType['prebuiltRulesFromSavedObjects'] -): Promise => { +): Promise => { // build a map of the most recent version of each rule const prepackaged = prebuiltRulesFromFileSystem ? getPrepackagedRules() : []; const ruleMap = new Map(prepackaged.map((r) => [r.rule_id, r])); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_install.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_install.ts index a9e22562606a9..332dc1756acca 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_install.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_install.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; +import { AddPrepackagedRulesSchema } from '../../../../common/detection_engine/schemas/request/rule_schemas'; import { RuleAlertType } from './types'; export const getRulesToInstall = ( - rulesFromFileSystem: AddPrepackagedRulesSchemaDecoded[], + rulesFromFileSystem: AddPrepackagedRulesSchema[], installedRules: RuleAlertType[] ) => { return rulesFromFileSystem.filter( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts index f0017c5e4bdb6..c5d7fa2fa9b79 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; +import { AddPrepackagedRulesSchema } from '../../../../common/detection_engine/schemas/request/rule_schemas'; import { RuleAlertType } from './types'; /** @@ -16,7 +16,7 @@ import { RuleAlertType } from './types'; * @param installedRules The installed rules */ export const getRulesToUpdate = ( - rulesFromFileSystem: AddPrepackagedRulesSchemaDecoded[], + rulesFromFileSystem: AddPrepackagedRulesSchema[], installedRules: RuleAlertType[] ) => { return rulesFromFileSystem @@ -31,7 +31,7 @@ export const getRulesToUpdate = ( * @param installedRules The installed rules to compare against for updates */ export const filterInstalledRules = ( - ruleFromFileSystem: AddPrepackagedRulesSchemaDecoded, + ruleFromFileSystem: AddPrepackagedRulesSchema, installedRules: RuleAlertType[] ): boolean => { return installedRules.some((installedRule) => { @@ -49,9 +49,9 @@ export const filterInstalledRules = ( * @param installedRules The installed rules which might have user driven exceptions_lists */ export const mergeExceptionLists = ( - ruleFromFileSystem: AddPrepackagedRulesSchemaDecoded, + ruleFromFileSystem: AddPrepackagedRulesSchema, installedRules: RuleAlertType[] -): AddPrepackagedRulesSchemaDecoded => { +): AddPrepackagedRulesSchema => { if (ruleFromFileSystem.exceptions_list != null) { const installedRule = installedRules.find( (ruleToFind) => ruleToFind.params.ruleId === ruleFromFileSystem.rule_id diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts index 7ca213817cf0c..15467fd82b22c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -7,133 +7,27 @@ import { SanitizedRule, RuleTypeParams } from '@kbn/alerting-plugin/common'; import { RulesClient } from '@kbn/alerting-plugin/server'; -import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; -import { createRules } from './create_rules'; -import { PartialFilter } from '../types'; +import { AddPrepackagedRulesSchema } from '../../../../common/detection_engine/schemas/request/rule_schemas'; +import { convertCreateAPIToInternalSchema } from '../schemas/rule_converters'; +import { AppClient } from '../../../types'; +import { InternalRuleCreate } from '../schemas/rule_schemas'; export const installPrepackagedRules = ( rulesClient: RulesClient, - rules: AddPrepackagedRulesSchemaDecoded[], - outputIndex: string + rules: AddPrepackagedRulesSchema[], + siemClient: AppClient ): Array>> => rules.reduce>>>((acc, rule) => { - const { - anomaly_threshold: anomalyThreshold, - author, - building_block_type: buildingBlockType, - description, - enabled, - timestamp_field: timestampField, - event_category_override: eventCategoryOverride, - tiebreaker_field: tiebreakerField, - false_positives: falsePositives, - from, - query, - language, - license, - machine_learning_job_id: machineLearningJobId, - saved_id: savedId, - timeline_id: timelineId, - timeline_title: timelineTitle, - meta, - filters: filtersObject, - rule_id: ruleId, - index, - data_view_id: dataViewId, - interval, - max_signals: maxSignals, - related_integrations: relatedIntegrations, - required_fields: requiredFields, - risk_score: riskScore, - risk_score_mapping: riskScoreMapping, - rule_name_override: ruleNameOverride, - name, - setup, - severity, - severity_mapping: severityMapping, - tags, - to, - type, - threat, - threat_filters: threatFilters, - threat_mapping: threatMapping, - threat_language: threatLanguage, - concurrent_searches: concurrentSearches, - items_per_search: itemsPerSearch, - threat_query: threatQuery, - threat_index: threatIndex, - threat_indicator_path: threatIndicatorPath, - threshold, - timestamp_override: timestampOverride, - references, - namespace, - note, - version, - exceptions_list: exceptionsList, - } = rule; - // TODO: Fix these either with an is conversion or by better typing them within io-ts - const filters: PartialFilter[] | undefined = filtersObject as PartialFilter[]; - + const internalRuleCreate: InternalRuleCreate = convertCreateAPIToInternalSchema( + rule, + siemClient, + true, + false + ); return [ ...acc, - createRules({ - rulesClient, - anomalyThreshold, - author, - buildingBlockType, - description, - enabled, - timestampField, - eventCategoryOverride, - tiebreakerField, - falsePositives, - from, - immutable: true, // At the moment we force all prepackaged rules to be immutable - query, - language, - license, - machineLearningJobId, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - ruleId, - index, - dataViewId, - interval, - maxSignals, - relatedIntegrations, - requiredFields, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - setup, - severity, - severityMapping, - tags, - to, - type, - threat, - threatFilters, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - threatQuery, - threatIndex, - threatIndicatorPath, - threshold, - throttle: null, // At this time there is no pre-packaged actions - timestampOverride, - references, - namespace, - note, - version, - exceptionsList, - actions: [], // At this time there is no pre-packaged actions + rulesClient.create({ + data: internalRuleCreate, }), ]; }, []); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index 5fc98e8cd30fc..442b253c788d8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -5,244 +5,52 @@ * 2.0. */ -import { validate } from '@kbn/securitysolution-io-ts-utils'; -import { defaults } from 'lodash/fp'; +import { BadRequestError } from '@kbn/securitysolution-es-utils'; import { PartialRule } from '@kbn/alerting-plugin/server'; -import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; -import { - normalizeMachineLearningJobIds, - normalizeThresholdObject, -} from '../../../../common/detection_engine/utils'; -import { internalRuleUpdate, RuleParams } from '../schemas/rule_schemas'; +import { RuleParams } from '../schemas/rule_schemas'; import { PatchRulesOptions } from './types'; -import { - calculateInterval, - calculateName, - calculateVersion, - maybeMute, - removeUndefined, - transformToAlertThrottle, - transformToNotifyWhen, -} from './utils'; - -class PatchError extends Error { - public readonly statusCode: number; - constructor(message: string, statusCode: number) { - super(message); - this.statusCode = statusCode; - } -} +import { maybeMute } from './utils'; +import { convertPatchAPIToInternalSchema } from '../schemas/rule_converters'; export const patchRules = async ({ rulesClient, - author, - buildingBlockType, - description, - timestampField, - eventCategoryOverride, - tiebreakerField, - falsePositives, - enabled, - query, - language, - license, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - from, - index, - dataViewId, - interval, - maxSignals, - relatedIntegrations, - requiredFields, - riskScore, - riskScoreMapping, - ruleNameOverride, rule, - name, - setup, - severity, - severityMapping, - tags, - threat, - threshold, - threatFilters, - threatIndex, - threatIndicatorPath, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - timestampOverride, - throttle, - to, - type, - references, - namespace, - note, - version, - exceptionsList, - anomalyThreshold, - machineLearningJobId, - actions, + params, }: PatchRulesOptions): Promise | null> => { if (rule == null) { return null; } - const calculatedVersion = calculateVersion(rule.params.immutable, rule.params.version, { - author, - buildingBlockType, - description, - timestampField, - eventCategoryOverride, - tiebreakerField, - falsePositives, - query, - language, - license, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - from, - index, - dataViewId, - interval, - maxSignals, - relatedIntegrations, - requiredFields, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - setup, - severity, - severityMapping, - tags, - threat, - threshold, - threatFilters, - threatIndex, - threatIndicatorPath, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - timestampOverride, - to, - type, - references, - version, - namespace, - note, - exceptionsList, - anomalyThreshold, - machineLearningJobId, - }); - - const nextParams = defaults( - { - ...rule.params, - }, - { - author, - buildingBlockType, - description, - timestampField, - eventCategoryOverride, - tiebreakerField, - falsePositives, - from, - query, - language, - license, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - index, - dataViewId, - maxSignals, - relatedIntegrations, - requiredFields, - riskScore, - riskScoreMapping, - ruleNameOverride, - setup, - severity, - severityMapping, - threat, - threshold: threshold ? normalizeThresholdObject(threshold) : undefined, - threatFilters, - threatIndex, - threatIndicatorPath, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - timestampOverride, - to, - type, - references, - namespace, - note, - version: calculatedVersion, - exceptionsList, - anomalyThreshold, - machineLearningJobId: machineLearningJobId - ? normalizeMachineLearningJobIds(machineLearningJobId) - : undefined, - } - ); - - const newRule = { - tags: tags ?? rule.tags, - name: calculateName({ updatedName: name, originalName: rule.name }), - schedule: { - interval: calculateInterval(interval, rule.schedule.interval), - }, - params: removeUndefined(nextParams), - actions: actions?.map(transformRuleToAlertAction) ?? rule.actions, - throttle: throttle !== undefined ? transformToAlertThrottle(throttle) : rule.throttle, - notifyWhen: throttle !== undefined ? transformToNotifyWhen(throttle) : rule.notifyWhen, - }; - - const [validated, errors] = validate(newRule, internalRuleUpdate); - if (errors != null || validated === null) { - throw new PatchError(`Applying patch would create invalid rule: ${errors}`, 400); + const patchedRule = convertPatchAPIToInternalSchema(params, rule); + // TODO: consistently throw or return the error - probably throw all the way from convert up to route handler + if (patchedRule instanceof BadRequestError) { + throw patchedRule; } const update = await rulesClient.update({ id: rule.id, - data: validated, + data: patchedRule, }); - if (throttle !== undefined) { - await maybeMute({ rulesClient, muteAll: rule.muteAll, throttle, id: update.id }); + if (params.throttle !== undefined) { + await maybeMute({ + rulesClient, + muteAll: rule.muteAll, + throttle: params.throttle, + id: update.id, + }); } - if (rule.enabled && enabled === false) { + if (rule.enabled && params.enabled === false) { await rulesClient.disable({ id: rule.id }); - } else if (!rule.enabled && enabled === true) { + } else if (!rule.enabled && params.enabled === true) { await rulesClient.enable({ id: rule.id }); } else { // enabled is null or undefined and we do not touch the rule } - if (enabled != null) { - return { ...update, enabled }; + if (params.enabled != null) { + return { ...update, enabled: params.enabled }; } else { return update; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 3d5b8849fc6b1..1da37d2487408 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -11,11 +11,8 @@ import { SavedObjectAttributes, SavedObjectsClientContract } from '@kbn/core/ser import type { MachineLearningJobIdOrUndefined, From, - FromOrUndefined, RiskScore, RiskScoreMapping, - RiskScoreMappingOrUndefined, - RiskScoreOrUndefined, ThreatIndexOrUndefined, ThreatQueryOrUndefined, ThreatMappingOrUndefined, @@ -25,23 +22,17 @@ import type { ItemsPerSearchOrUndefined, ThreatIndicatorPathOrUndefined, Threats, - ThreatsOrUndefined, - TypeOrUndefined, Type, LanguageOrUndefined, SeverityMapping, - SeverityMappingOrUndefined, - SeverityOrUndefined, Severity, - MaxSignalsOrUndefined, MaxSignals, - ThrottleOrUndefinedOrNull, ThrottleOrNull, } from '@kbn/securitysolution-io-ts-alerting-types'; -import type { VersionOrUndefined, Version } from '@kbn/securitysolution-io-ts-types'; +import type { Version } from '@kbn/securitysolution-io-ts-types'; import { ruleTypeMappings } from '@kbn/securitysolution-rules'; -import type { ListArrayOrUndefined, ListArray } from '@kbn/securitysolution-io-ts-list-types'; +import type { ListArray } from '@kbn/securitysolution-io-ts-list-types'; import { RulesClient, PartialRule, BulkEditOperation } from '@kbn/alerting-plugin/server'; import { SanitizedRule } from '@kbn/alerting-plugin/common'; import { UpdateRulesSchema } from '../../../../common/detection_engine/schemas/request'; @@ -50,7 +41,6 @@ import { FalsePositives, RuleId, Immutable, - DescriptionOrUndefined, Interval, OutputIndex, Name, @@ -70,15 +60,7 @@ import { Id, IdOrUndefined, RuleIdOrUndefined, - EnabledOrUndefined, - FalsePositivesOrUndefined, - OutputIndexOrUndefined, - IntervalOrUndefined, - NameOrUndefined, - TagsOrUndefined, - ToOrUndefined, ThresholdOrUndefined, - ReferencesOrUndefined, PerPageOrUndefined, PageOrUndefined, SortFieldOrUndefined, @@ -86,7 +68,6 @@ import { FieldsOrUndefined, SortOrderOrUndefined, Author, - AuthorOrUndefined, LicenseOrUndefined, TimestampOverrideOrUndefined, BuildingBlockTypeOrUndefined, @@ -104,6 +85,7 @@ import { import { PartialFilter } from '../types'; import { RuleParams } from '../schemas/rule_schemas'; import { IRuleExecutionLogForRoutes } from '../rule_execution_log'; +import { PatchRulesSchema } from '../../../../common/detection_engine/schemas/request/rule_schemas'; export type RuleAlertType = SanitizedRule; @@ -211,68 +193,12 @@ export interface UpdateRulesOptions { ruleUpdate: UpdateRulesSchema; } -export interface PatchRulesOptions extends Partial { +export interface PatchRulesOptions { rulesClient: RulesClient; + params: PatchRulesSchema; rule: RuleAlertType | null | undefined; } -interface PatchRulesFieldsOptions { - anomalyThreshold: AnomalyThresholdOrUndefined; - author: AuthorOrUndefined; - buildingBlockType: BuildingBlockTypeOrUndefined; - description: DescriptionOrUndefined; - enabled: EnabledOrUndefined; - timestampField: TimestampFieldOrUndefined; - eventCategoryOverride: EventCategoryOverrideOrUndefined; - tiebreakerField: TiebreakerFieldOrUndefined; - falsePositives: FalsePositivesOrUndefined; - from: FromOrUndefined; - query: QueryOrUndefined; - language: LanguageOrUndefined; - savedId: SavedIdOrUndefined; - timelineId: TimelineIdOrUndefined; - timelineTitle: TimelineTitleOrUndefined; - meta: MetaOrUndefined; - machineLearningJobId: MachineLearningJobIdOrUndefined; - filters: PartialFilter[]; - index: IndexOrUndefined; - dataViewId: DataViewIdOrUndefined; - interval: IntervalOrUndefined; - license: LicenseOrUndefined; - maxSignals: MaxSignalsOrUndefined; - relatedIntegrations: RelatedIntegrationArray | undefined; - requiredFields: RequiredFieldArray | undefined; - riskScore: RiskScoreOrUndefined; - riskScoreMapping: RiskScoreMappingOrUndefined; - ruleNameOverride: RuleNameOverrideOrUndefined; - outputIndex: OutputIndexOrUndefined; - name: NameOrUndefined; - setup: SetupGuide | undefined; - severity: SeverityOrUndefined; - severityMapping: SeverityMappingOrUndefined; - tags: TagsOrUndefined; - threat: ThreatsOrUndefined; - itemsPerSearch: ItemsPerSearchOrUndefined; - concurrentSearches: ConcurrentSearchesOrUndefined; - threshold: ThresholdOrUndefined; - threatFilters: ThreatFiltersOrUndefined; - threatIndex: ThreatIndexOrUndefined; - threatIndicatorPath: ThreatIndicatorPathOrUndefined; - threatQuery: ThreatQueryOrUndefined; - threatMapping: ThreatMappingOrUndefined; - threatLanguage: ThreatLanguageOrUndefined; - throttle: ThrottleOrUndefinedOrNull; - timestampOverride: TimestampOverrideOrUndefined; - to: ToOrUndefined; - type: TypeOrUndefined; - references: ReferencesOrUndefined; - note: NoteOrUndefined; - version: VersionOrUndefined; - exceptionsList: ListArrayOrUndefined; - actions: RuleAlertAction[] | undefined; - namespace?: NamespaceOrUndefined; -} - export interface ReadRuleOptions { rulesClient: RulesClient; id: IdOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts index f64383d36e11e..754e4b1fa90af 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -8,18 +8,17 @@ import { chunk } from 'lodash/fp'; import { SavedObjectsClientContract } from '@kbn/core/server'; import { RulesClient, PartialRule } from '@kbn/alerting-plugin/server'; -import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; +import { AddPrepackagedRulesSchema } from '../../../../common/detection_engine/schemas/request/rule_schemas'; import { MAX_RULES_TO_UPDATE_IN_PARALLEL } from '../../../../common/constants'; import { patchRules } from './patch_rules'; import { readRules } from './read_rules'; -import { PartialFilter } from '../types'; -import { RuleParams } from '../schemas/rule_schemas'; +import { InternalRuleCreate, RuleParams } from '../schemas/rule_schemas'; import { legacyMigrate } from './utils'; import { deleteRules } from './delete_rules'; import { PrepackagedRulesError } from '../routes/rules/add_prepackaged_rules_route'; import { IRuleExecutionLogForRoutes } from '../rule_execution_log'; -import { createRules } from './create_rules'; -import { transformAlertToRuleAction } from '../../../../common/detection_engine/transform_actions'; +import { AppClient } from '../../../types'; +import { convertCreateAPIToInternalSchema } from '../schemas/rule_converters'; /** * Updates the prepackaged rules given a set of rules and output index. @@ -33,8 +32,8 @@ import { transformAlertToRuleAction } from '../../../../common/detection_engine/ export const updatePrepackagedRules = async ( rulesClient: RulesClient, savedObjectsClient: SavedObjectsClientContract, - rules: AddPrepackagedRulesSchemaDecoded[], - outputIndex: string, + rules: AddPrepackagedRulesSchema[], + siemClient: AppClient, ruleExecutionLog: IRuleExecutionLogForRoutes ): Promise => { const ruleChunks = chunk(MAX_RULES_TO_UPDATE_IN_PARALLEL, rules); @@ -43,7 +42,7 @@ export const updatePrepackagedRules = async ( rulesClient, savedObjectsClient, ruleChunk, - outputIndex, + siemClient, ruleExecutionLog ); await Promise.all(rulePromises); @@ -61,74 +60,17 @@ export const updatePrepackagedRules = async ( export const createPromises = ( rulesClient: RulesClient, savedObjectsClient: SavedObjectsClientContract, - rules: AddPrepackagedRulesSchemaDecoded[], - outputIndex: string, + rules: AddPrepackagedRulesSchema[], + siemClient: AppClient, ruleExecutionLog: IRuleExecutionLogForRoutes ): Array | null>> => { return rules.map(async (rule) => { - const { - author, - building_block_type: buildingBlockType, - description, - timestamp_field: timestampField, - event_category_override: eventCategoryOverride, - tiebreaker_field: tiebreakerField, - false_positives: falsePositives, - from, - query, - language, - license, - saved_id: savedId, - meta, - filters: filtersObject, - rule_id: ruleId, - index, - data_view_id: dataViewId, - interval, - max_signals: maxSignals, - related_integrations: relatedIntegrations, - required_fields: requiredFields, - risk_score: riskScore, - risk_score_mapping: riskScoreMapping, - rule_name_override: ruleNameOverride, - name, - setup, - severity, - severity_mapping: severityMapping, - tags, - to, - type, - threat, - threshold, - threat_filters: threatFilters, - threat_index: threatIndex, - threat_indicator_path: threatIndicatorPath, - threat_query: threatQuery, - threat_mapping: threatMapping, - threat_language: threatLanguage, - concurrent_searches: concurrentSearches, - items_per_search: itemsPerSearch, - timestamp_override: timestampOverride, - references, - version, - note, - throttle, - anomaly_threshold: anomalyThreshold, - timeline_id: timelineId, - timeline_title: timelineTitle, - machine_learning_job_id: machineLearningJobId, - exceptions_list: exceptionsList, - } = rule; - const existingRule = await readRules({ rulesClient, - ruleId, + ruleId: rule.rule_id, id: undefined, }); - // TODO: Fix these either with an is conversion or by better typing them within io-ts - const filters: PartialFilter[] | undefined = filtersObject as PartialFilter[]; - const migratedRule = await legacyMigrate({ rulesClient, savedObjectsClient, @@ -136,138 +78,38 @@ export const createPromises = ( }); if (!migratedRule) { - throw new PrepackagedRulesError(`Failed to find rule ${ruleId}`, 500); + throw new PrepackagedRulesError(`Failed to find rule ${rule.rule_id}`, 500); } // If we're trying to change the type of a prepackaged rule, we need to delete the old one // and replace it with the new rule, keeping the enabled setting, actions, throttle, id, // and exception lists from the old rule - if (type !== migratedRule.params.type) { + if (rule.type !== migratedRule.params.type) { await deleteRules({ ruleId: migratedRule.id, rulesClient, ruleExecutionLog, }); - return (await createRules({ - id: migratedRule.id, - rulesClient, - anomalyThreshold, - author, - buildingBlockType, - description, - enabled: migratedRule.enabled, // Enabled comes from existing rule - timestampField, - eventCategoryOverride, - tiebreakerField, - falsePositives, - from, - immutable: true, // At the moment we force all prepackaged rules to be immutable - query, - language, - license, - machineLearningJobId, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - ruleId, - index, - dataViewId, - interval, - maxSignals, - relatedIntegrations, - requiredFields, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - setup, - severity, - severityMapping, - tags, - to, - type, - threat, - threatFilters, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - threatQuery, - threatIndex, - threatIndicatorPath, - threshold, - throttle: migratedRule.throttle, // Throttle comes from the existing rule - timestampOverride, - references, - note, - version, - // The exceptions list passed in to this function has already been merged with the exceptions list of - // the existing rule - exceptionsList, - actions: migratedRule.actions.map(transformAlertToRuleAction), // Actions come from the existing rule - })) as PartialRule; // TODO: Replace AddPrepackagedRulesSchema with type specific rules schema so we can clean up these types + const internalRuleCreate: InternalRuleCreate = { + ...convertCreateAPIToInternalSchema(rule, siemClient, true), + // Force the prepackaged rule to use the enabled state from the existing rule, + // regardless of what the prepackaged rule says + enabled: migratedRule.enabled, + }; + + return rulesClient.create({ + data: internalRuleCreate, + }); } else { - // Note: we do not pass down enabled as we do not want to suddenly disable - // or enable rules on the user when they were not expecting it if a rule updates return patchRules({ rulesClient, - author, - buildingBlockType, - description, - timestampField, - eventCategoryOverride, - tiebreakerField, - falsePositives, - from, - query, - language, - license, - outputIndex, rule: migratedRule, - savedId, - meta, - filters, - index, - interval, - maxSignals, - relatedIntegrations, - requiredFields, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - setup, - severity, - severityMapping, - tags, - timestampOverride, - to, - type, - threat, - threshold, - threatFilters, - threatIndex, - threatIndicatorPath, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - references, - version, - note, - anomalyThreshold, - enabled: undefined, - timelineId, - timelineTitle, - machineLearningJobId, - exceptionsList, - throttle, - actions: undefined, + params: { + ...rule, + // Again, force enabled to use the enabled state from the existing rule + enabled: migratedRule.enabled, + }, }); } }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts index 7c981a5481ff9..82a44a70583f1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts @@ -6,24 +6,15 @@ */ /* eslint-disable complexity */ -import { validate } from '@kbn/securitysolution-io-ts-utils'; import { PartialRule } from '@kbn/alerting-plugin/server'; import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { UpdateRulesOptions } from './types'; import { typeSpecificSnakeToCamel } from '../schemas/rule_converters'; -import { internalRuleUpdate, RuleParams } from '../schemas/rule_schemas'; +import { InternalRuleUpdate, RuleParams } from '../schemas/rule_schemas'; import { maybeMute, transformToAlertThrottle, transformToNotifyWhen } from './utils'; -class UpdateError extends Error { - public readonly statusCode: number; - constructor(message: string, statusCode: number) { - super(message); - this.statusCode = statusCode; - } -} - export const updateRules = async ({ rulesClient, defaultOutputIndex, @@ -36,7 +27,7 @@ export const updateRules = async ({ const typeSpecificParams = typeSpecificSnakeToCamel(ruleUpdate); const enabled = ruleUpdate.enabled ?? true; - const newInternalRule = { + const newInternalRule: InternalRuleUpdate = { name: ruleUpdate.name, tags: ruleUpdate.tags ?? [], params: { @@ -83,14 +74,9 @@ export const updateRules = async ({ notifyWhen: transformToNotifyWhen(ruleUpdate.throttle), }; - const [validated, errors] = validate(newInternalRule, internalRuleUpdate); - if (errors != null || validated === null) { - throw new UpdateError(`Applying update would create invalid rule: ${errors}`, 400); - } - const update = await rulesClient.update({ id: existingRule.id, - data: validated, + data: newInternalRule, }); await maybeMute({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts index b615e705b556b..105e1e1c5764e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts @@ -6,8 +6,13 @@ */ import uuid from 'uuid'; +import * as t from 'io-ts'; +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { BadRequestError } from '@kbn/securitysolution-es-utils'; import { ruleTypeMappings } from '@kbn/securitysolution-rules'; +import { exactCheck, formatErrors } from '@kbn/securitysolution-io-ts-utils'; import { ResolvedSanitizedRule, SanitizedRule } from '@kbn/alerting-plugin/common'; import { @@ -19,15 +24,41 @@ import { RuleParams, TypeSpecificRuleParams, BaseRuleParams, + EqlRuleParams, + EqlSpecificRuleParams, + ThreatRuleParams, + ThreatSpecificRuleParams, + QueryRuleParams, + QuerySpecificRuleParams, + SavedQuerySpecificRuleParams, + SavedQueryRuleParams, + ThresholdRuleParams, + ThresholdSpecificRuleParams, + MachineLearningRuleParams, + MachineLearningSpecificRuleParams, + InternalRuleUpdate, } from './rule_schemas'; import { assertUnreachable } from '../../../../common/utility_types'; import { RuleExecutionSummary } from '../../../../common/detection_engine/schemas/common'; import { CreateRulesSchema, CreateTypeSpecific, + eqlFullPatchSchema, + EqlPatchParams, FullResponseSchema, + machineLearningFullPatchSchema, + MachineLearningPatchParams, + queryFullPatchSchema, + QueryPatchParams, ResponseTypeSpecific, + savedQueryFullPatchSchema, + SavedQueryPatchParams, + threatMatchFullPatchSchema, + ThreatMatchPatchParams, + thresholdFullPatchSchema, + ThresholdPatchParams, } from '../../../../common/detection_engine/schemas/request'; +import { PatchRulesSchema } from '../../../../common/detection_engine/schemas/request/rule_schemas'; import { AppClient } from '../../../types'; import { DEFAULT_MAX_SIGNALS, SERVER_APP_ID } from '../../../../common/constants'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; @@ -130,9 +161,252 @@ export const typeSpecificSnakeToCamel = (params: CreateTypeSpecific): TypeSpecif } }; +const validateSchema = (input: unknown, schema: t.Type): A | BadRequestError => { + const decoded = schema.decode(input); + const checked = exactCheck(input, decoded); + const onLeft = (errors: t.Errors): BadRequestError | A => { + return new BadRequestError(formatErrors(errors).join()); + }; + const onRight = (value: A): BadRequestError | A => { + return value; + }; + return pipe(checked, fold(onLeft, onRight)); +}; + +const patchEqlParams = ( + params: EqlPatchParams, + existingRule: EqlRuleParams +): EqlSpecificRuleParams => { + return { + type: existingRule.type, + language: params.language ?? existingRule.language, + index: params.index ?? existingRule.index, + dataViewId: params.data_view_id ?? existingRule.dataViewId, + query: params.query ?? existingRule.query, + filters: params.filters ?? existingRule.filters, + timestampField: params.timestamp_field ?? existingRule.timestampField, + eventCategoryOverride: params.event_category_override ?? existingRule.eventCategoryOverride, + tiebreakerField: params.tiebreaker_field ?? existingRule.tiebreakerField, + }; +}; + +const patchThreatMatchParams = ( + params: ThreatMatchPatchParams, + existingRule: ThreatRuleParams +): ThreatSpecificRuleParams => { + return { + type: existingRule.type, + language: params.language ?? existingRule.language, + index: params.index ?? existingRule.index, + dataViewId: params.data_view_id ?? existingRule.dataViewId, + query: params.query ?? existingRule.query, + filters: params.filters ?? existingRule.filters, + savedId: params.saved_id ?? existingRule.savedId, + threatFilters: params.threat_filters ?? existingRule.threatFilters, + threatQuery: params.threat_query ?? existingRule.threatQuery, + threatMapping: params.threat_mapping ?? existingRule.threatMapping, + threatLanguage: params.threat_language ?? existingRule.threatLanguage, + threatIndex: params.threat_index ?? existingRule.threatIndex, + threatIndicatorPath: params.threat_indicator_path ?? existingRule.threatIndicatorPath, + concurrentSearches: params.concurrent_searches ?? existingRule.concurrentSearches, + itemsPerSearch: params.items_per_search ?? existingRule.itemsPerSearch, + }; +}; + +const patchQueryParams = ( + params: QueryPatchParams, + existingRule: QueryRuleParams +): QuerySpecificRuleParams => { + return { + type: existingRule.type, + language: params.language ?? existingRule.language, + index: params.index ?? existingRule.index, + dataViewId: params.data_view_id ?? existingRule.dataViewId, + query: params.query ?? existingRule.query, + filters: params.filters ?? existingRule.filters, + savedId: params.saved_id ?? existingRule.savedId, + }; +}; + +const patchSavedQueryParams = ( + params: SavedQueryPatchParams, + existingRule: SavedQueryRuleParams +): SavedQuerySpecificRuleParams => { + return { + type: existingRule.type, + language: params.language ?? existingRule.language, + index: params.index ?? existingRule.index, + dataViewId: params.data_view_id ?? existingRule.dataViewId, + query: params.query ?? existingRule.query, + filters: params.filters ?? existingRule.filters, + savedId: params.saved_id ?? existingRule.savedId, + }; +}; + +const patchThresholdParams = ( + params: ThresholdPatchParams, + existingRule: ThresholdRuleParams +): ThresholdSpecificRuleParams => { + return { + type: existingRule.type, + language: params.language ?? existingRule.language, + index: params.index ?? existingRule.index, + dataViewId: params.data_view_id ?? existingRule.dataViewId, + query: params.query ?? existingRule.query, + filters: params.filters ?? existingRule.filters, + savedId: params.saved_id ?? existingRule.savedId, + threshold: params.threshold + ? normalizeThresholdObject(params.threshold) + : existingRule.threshold, + }; +}; + +const patchMachineLearningParams = ( + params: MachineLearningPatchParams, + existingRule: MachineLearningRuleParams +): MachineLearningSpecificRuleParams => { + return { + type: existingRule.type, + anomalyThreshold: params.anomaly_threshold ?? existingRule.anomalyThreshold, + machineLearningJobId: params.machine_learning_job_id + ? normalizeMachineLearningJobIds(params.machine_learning_job_id) + : existingRule.machineLearningJobId, + }; +}; + +// TODO: unit test this function to make sure schemas pass validation as expected +export const patchTypeSpecificSnakeToCamel = ( + params: PatchRulesSchema, + existingRule: RuleParams +): TypeSpecificRuleParams | BadRequestError => { + // Each rule type validates the full patch schema for the specific type of rule to ensure that + // params from other rule types are not being passed in. Otherwise, since the `type` is not required + // on patch requests, type specific params from one rule type could be passed in when patching a + // different rule type and they'd be ignored without a warning or error. + // This validation ensures that e.g. only valid EQL rule params are passed in when patching an EQL rule. + switch (existingRule.type) { + case 'eql': { + const validated = validateSchema(params, eqlFullPatchSchema); + if (validated instanceof BadRequestError) { + return validated; + } + return patchEqlParams(validated, existingRule); + } + case 'threat_match': { + const validated = validateSchema(params, threatMatchFullPatchSchema); + if (validated instanceof BadRequestError) { + return validated; + } + return patchThreatMatchParams(validated, existingRule); + } + case 'query': { + const validated = validateSchema(params, queryFullPatchSchema); + if (validated instanceof BadRequestError) { + return validated; + } + return patchQueryParams(validated, existingRule); + } + case 'saved_query': { + const validated = validateSchema(params, savedQueryFullPatchSchema); + if (validated instanceof BadRequestError) { + return validated; + } + return patchSavedQueryParams(validated, existingRule); + } + case 'threshold': { + const validated = validateSchema(params, thresholdFullPatchSchema); + if (validated instanceof BadRequestError) { + return validated; + } + return patchThresholdParams(validated, existingRule); + } + case 'machine_learning': { + const validated = validateSchema(params, machineLearningFullPatchSchema); + if (validated instanceof BadRequestError) { + return validated; + } + return patchMachineLearningParams(validated, existingRule); + } + default: { + return assertUnreachable(existingRule); + } + } +}; +const versionExcludedKeys = ['enabled', 'id', 'rule_id']; +const shouldUpdateVersion = (params: PatchRulesSchema): boolean => { + for (const key in params) { + if (!versionExcludedKeys.includes(key)) { + return true; + } + } + return false; +}; + +// TODO: tests to ensure version gets updated as expected +// eslint-disable-next-line complexity +export const convertPatchAPIToInternalSchema = ( + params: PatchRulesSchema, + existingRule: SanitizedRule +): InternalRuleUpdate | BadRequestError => { + const typeSpecificParams = patchTypeSpecificSnakeToCamel(params, existingRule.params); + if (typeSpecificParams instanceof BadRequestError) { + return typeSpecificParams; + } + const existingParams = existingRule.params; + return { + name: params.name ?? existingRule.name, + tags: params.tags ?? existingRule.tags, + params: { + author: params.author ?? existingParams.author, + buildingBlockType: params.building_block_type ?? existingParams.buildingBlockType, + description: params.description ?? existingParams.description, + ruleId: existingParams.ruleId, + falsePositives: params.false_positives ?? existingParams.falsePositives, + from: params.from ?? existingParams.from, + immutable: existingParams.immutable, + license: params.license ?? existingParams.license, + outputIndex: params.output_index ?? existingParams.outputIndex, + timelineId: params.timeline_id ?? existingParams.timelineId, + timelineTitle: params.timeline_title ?? existingParams.timelineTitle, + meta: params.meta ?? existingParams.meta, + maxSignals: params.max_signals ?? existingParams.maxSignals, + relatedIntegrations: existingParams.relatedIntegrations, + requiredFields: existingParams.requiredFields, + riskScore: params.risk_score ?? existingParams.riskScore, + riskScoreMapping: params.risk_score_mapping ?? existingParams.riskScoreMapping, + ruleNameOverride: params.rule_name_override ?? existingParams.ruleNameOverride, + setup: existingParams.setup, + severity: params.severity ?? existingParams.severity, + severityMapping: params.severity_mapping ?? existingParams.severityMapping, + threat: params.threat ?? existingParams.threat, + timestampOverride: params.timestamp_override ?? existingParams.timestampOverride, + to: params.to ?? existingParams.to, + references: params.references ?? existingParams.references, + namespace: params.namespace ?? existingParams.namespace, + note: params.note ?? existingParams.note, + // Always use the version from the request if specified. If it isn't specified, leave immutable rules alone and + // increment the version of mutable rules by 1. + version: + params.version ?? existingParams.immutable + ? existingParams.version + : shouldUpdateVersion(params) + ? existingParams.version + 1 + : existingParams.version, + exceptionsList: params.exceptions_list ?? existingParams.exceptionsList, + ...typeSpecificParams, + }, + schedule: { interval: params.interval ?? existingRule.schedule.interval }, + actions: params.actions ? params.actions.map(transformRuleToAlertAction) : existingRule.actions, + throttle: params.throttle ? transformToAlertThrottle(params.throttle) : existingRule.throttle, + notifyWhen: params.throttle ? transformToNotifyWhen(params.throttle) : existingRule.notifyWhen, + }; +}; + export const convertCreateAPIToInternalSchema = ( input: CreateRulesSchema, - siemClient: AppClient + siemClient: AppClient, + immutable = false, + defaultEnabled = true ): InternalRuleCreate => { const typeSpecificParams = typeSpecificSnakeToCamel(input); const newRuleId = input.rule_id ?? uuid.v4(); @@ -148,7 +422,7 @@ export const convertCreateAPIToInternalSchema = ( ruleId: newRuleId, falsePositives: input.false_positives ?? [], from: input.from ?? 'now-6m', - immutable: false, + immutable, license: input.license, outputIndex: input.output_index ?? siemClient.getSignalsIndex(), timelineId: input.timeline_id, @@ -174,7 +448,7 @@ export const convertCreateAPIToInternalSchema = ( ...typeSpecificParams, }, schedule: { interval: input.interval ?? '5m' }, - enabled: input.enabled ?? true, + enabled: input.enabled ?? defaultEnabled, actions: input.actions?.map(transformRuleToAlertAction) ?? [], throttle: transformToAlertThrottle(input.throttle), notifyWhen: transformToNotifyWhen(input.throttle), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts index fa4d912368d33..4bc65a21f620b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts @@ -131,6 +131,7 @@ const eqlSpecificRuleParams = t.type({ tiebreakerField: tiebreakerFieldOrUndefined, }); export const eqlRuleParams = t.intersection([baseRuleParams, eqlSpecificRuleParams]); +export type EqlSpecificRuleParams = t.TypeOf; export type EqlRuleParams = t.TypeOf; const threatSpecificRuleParams = t.type({ @@ -151,6 +152,7 @@ const threatSpecificRuleParams = t.type({ dataViewId: dataViewIdOrUndefined, }); export const threatRuleParams = t.intersection([baseRuleParams, threatSpecificRuleParams]); +export type ThreatSpecificRuleParams = t.TypeOf; export type ThreatRuleParams = t.TypeOf; const querySpecificRuleParams = t.exact( @@ -165,6 +167,7 @@ const querySpecificRuleParams = t.exact( }) ); export const queryRuleParams = t.intersection([baseRuleParams, querySpecificRuleParams]); +export type QuerySpecificRuleParams = t.TypeOf; export type QueryRuleParams = t.TypeOf; const savedQuerySpecificRuleParams = t.type({ @@ -179,6 +182,7 @@ const savedQuerySpecificRuleParams = t.type({ savedId: saved_id, }); export const savedQueryRuleParams = t.intersection([baseRuleParams, savedQuerySpecificRuleParams]); +export type SavedQuerySpecificRuleParams = t.TypeOf; export type SavedQueryRuleParams = t.TypeOf; const thresholdSpecificRuleParams = t.type({ @@ -192,6 +196,7 @@ const thresholdSpecificRuleParams = t.type({ dataViewId: dataViewIdOrUndefined, }); export const thresholdRuleParams = t.intersection([baseRuleParams, thresholdSpecificRuleParams]); +export type ThresholdSpecificRuleParams = t.TypeOf; export type ThresholdRuleParams = t.TypeOf; const machineLearningSpecificRuleParams = t.type({ @@ -203,6 +208,7 @@ export const machineLearningRuleParams = t.intersection([ baseRuleParams, machineLearningSpecificRuleParams, ]); +export type MachineLearningSpecificRuleParams = t.TypeOf; export type MachineLearningRuleParams = t.TypeOf; export const typeSpecificRuleParams = t.union([