From 7db7782741115288968ae97604da4d5137d44894 Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Tue, 3 Nov 2020 11:49:02 -0500 Subject: [PATCH 01/19] Adds framework for replacing API schemas --- .../schemas/common/schemas.ts | 35 +- .../schemas/request/rule_schemas.mock.ts | 18 + .../schemas/request/rule_schemas.ts | 413 ++++++++++++++++++ .../detection_engine/rules/api.test.ts | 4 +- .../containers/detection_engine/rules/api.ts | 5 +- .../detection_engine/rules/types.ts | 4 +- .../rules/use_create_rule.test.tsx | 6 +- .../rules/use_create_rule.tsx | 6 +- .../detection_engine/rules/create/index.tsx | 4 +- .../routes/rules/create_rules_route.test.ts | 4 +- .../routes/rules/create_rules_route.ts | 227 ++++------ .../detection_engine/routes/rules/validate.ts | 17 + .../schemas/rule_converters.ts | 81 ++++ .../detection_engine/schemas/rule_schemas.ts | 178 ++++++++ 14 files changed, 844 insertions(+), 158 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index e8d7f409de20a2..fe4ef428d66889 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -9,6 +9,11 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; +import { + SavedObjectAttributes, + SavedObjectAttribute, + SavedObjectAttributeSingle, +} from 'src/core/types'; import { RiskScore } from '../types/risk_score'; import { UUID } from '../types/uuid'; import { IsoDateString } from '../types/iso_date_string'; @@ -65,6 +70,22 @@ export type ExcludeExportDetails = t.TypeOf; export const filters = t.array(t.unknown); // Filters are not easily type-able yet export type Filters = t.TypeOf; // Filters are not easily type-able yet +export const filtersOrUndefined = t.union([filters, t.undefined]); +export type FiltersOrUndefined = t.TypeOf; + +export const saved_object_attribute_single: t.Type = t.recursion( + 'saved_object_attribute_single', + () => t.union([t.string, t.number, t.boolean, t.null, t.undefined, saved_object_attributes]) +); +export const saved_object_attribute: t.Type = t.recursion( + 'saved_object_attribute', + () => t.union([saved_object_attribute_single, t.array(saved_object_attribute_single)]) +); +export const saved_object_attributes: t.Type = t.recursion( + 'saved_object_attributes', + () => t.record(t.string, saved_object_attribute) +); + /** * Params is an "object", since it is a type of AlertActionParams which is action templates. * @see x-pack/plugins/alerts/common/alert.ts @@ -72,7 +93,7 @@ export type Filters = t.TypeOf; // Filters are not easily type-a export const action_group = t.string; export const action_id = t.string; export const action_action_type_id = t.string; -export const action_params = t.object; +export const action_params = saved_object_attributes; export const action = t.exact( t.type({ group: action_group, @@ -85,6 +106,18 @@ export const action = t.exact( export const actions = t.array(action); export type Actions = t.TypeOf; +export const actionsCamel = t.array( + t.exact( + t.type({ + group: action_group, + id: action_id, + actionTypeId: action_action_type_id, + params: action_params, + }) + ) +); +export type ActionsCamel = t.TypeOf; + const stringValidator = (input: unknown): input is string => typeof input === 'string'; export const from = new t.Type( 'From', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts new file mode 100644 index 00000000000000..e13b7cd46245ab --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FullCreateSchema } from './rule_schemas'; + +export const getFullCreateSchemaMock = (ruleId = 'rule-1'): FullCreateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'query', + risk_score: 55, + language: 'kuery', + rule_id: ruleId, +}); 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 new file mode 100644 index 00000000000000..9c01f70ec7f10f --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -0,0 +1,413 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +import { listArray } from '../types/lists'; +import { + threat_filters, + threat_query, + threat_mapping, + threat_language, +} from '../types/threat_mapping'; +import { + id, + index, + filters, + event_category_override, + risk_score_mapping, + severity_mapping, + type, + building_block_type, + note, + license, + timeline_id, + timeline_title, + meta, + rule_name_override, + timestamp_override, + author, + description, + false_positives, + from, + rule_id, + immutable, + indexOrUndefined, + language, + output_index, + query, + filtersOrUndefined, + machine_learning_job_id, + max_signals, + risk_score, + severity, + threat, + to, + references, + version, + savedIdOrUndefined, + saved_id, + threshold, + anomaly_threshold, + name, + tags, + actions, + interval, + enabled, + updated_by, + updated_at, + created_by, + created_at, + job_status, + status_date, + last_success_at, + last_success_message, + last_failure_at, + last_failure_message, + throttleOrNull, +} from '../common/schemas'; + +const createUpdateSchema = < + Required extends t.Props, + Optional extends t.Props, + Defaultable extends t.Props +>( + requiredFields: Required, + optionalFields: Optional, + defaultableFields: Defaultable +) => { + return t.intersection([ + t.exact(t.type(requiredFields)), + t.exact(t.partial(optionalFields)), + t.exact(t.partial(defaultableFields)), + ]); +}; + +const patchSchema = < + Required extends t.Props, + Optional extends t.Props, + Defaultable extends t.Props +>( + requiredFields: Required, + optionalFields: Optional, + defaultableFields: Defaultable +) => { + return t.intersection([ + t.exact(t.partial(requiredFields)), + t.exact(t.partial(optionalFields)), + t.exact(t.partial(defaultableFields)), + ]); +}; + +const responseSchema = < + Required extends t.Props, + Optional extends t.Props, + Defaultable extends t.Props +>( + requiredFields: Required, + optionalFields: Optional, + defaultableFields: Defaultable +) => { + return t.intersection([ + t.exact(t.type(requiredFields)), + t.exact(t.partial(optionalFields)), + t.exact(t.type(defaultableFields)), + ]); +}; + +const requiredRuleFields = { + description, + risk_score, + name, + severity, + type, +}; + +const optionalRuleFields = { + building_block_type, + description, + note, + license, + output_index, + timeline_id, + timeline_title, + meta, + rule_name_override, + severity, + timestamp_override, +}; + +const defaultableRuleFields = { + author, + false_positives, + from, + rule_id, + // maxSignals not used in ML rules but probably should be used + max_signals, + risk_score_mapping, + severity_mapping, + threat, + to, + references, + version, + exceptions_list: listArray, +}; + +const createUpdateRuleParams = createUpdateSchema( + requiredRuleFields, + optionalRuleFields, + defaultableRuleFields +); +const patchRuleParams = patchSchema(requiredRuleFields, optionalRuleFields, defaultableRuleFields); +const responseRuleParams = responseSchema( + requiredRuleFields, + optionalRuleFields, + defaultableRuleFields +); + +const eqlRequiredFields = { + type: t.literal('eql'), + language: t.literal('eql'), + query, +}; + +const eqlOptionalFields = { + index, + filters, + event_category_override, +}; + +const createUpdateEqlParams = createUpdateSchema(eqlRequiredFields, eqlOptionalFields, {}); +const patchEqlParams = patchSchema(eqlRequiredFields, eqlOptionalFields, {}); +const responseEqlParams = responseSchema(eqlRequiredFields, eqlOptionalFields, {}); + +const threatMatchRequiredFields = { + type: t.literal('threat_match'), + language: t.keyof({ kuery: null, lucene: null }), + query, +}; + +const threatMatchOptionalFields = { + index, + filters, + saved_id, + threat_filters, + threat_query, + threat_mapping, + threat_language, +}; + +const createUpdateThreatMatchParams = createUpdateSchema( + threatMatchRequiredFields, + threatMatchOptionalFields, + {} +); +const patchThreatMatchParams = patchSchema( + threatMatchRequiredFields, + threatMatchOptionalFields, + {} +); +const responseThreatMatchParams = responseSchema( + threatMatchRequiredFields, + threatMatchOptionalFields, + {} +); + +const queryRequiredFields = { + type: t.literal('query'), + language: t.keyof({ kuery: null, lucene: null }), + query, +}; + +const queryOptionalFields = { + index, + filters, + saved_id, +}; + +const createUpdateQueryParams = createUpdateSchema(queryRequiredFields, queryOptionalFields, {}); +const patchQueryParams = patchSchema(queryRequiredFields, queryOptionalFields, {}); +const responseQueryParams = responseSchema(queryRequiredFields, queryOptionalFields, {}); + +const savedQueryRequiredFields = { + type: t.literal('saved_query'), + saved_id, +}; + +const savedQueryOptionalFields = { + // Having language, query, and filters possibly defined adds more code confusion and probably user confusion + // if the saved object gets deleted for some reason + language, + index, + query, + filters, +}; + +const createUpdateSavedQueryParams = createUpdateSchema( + savedQueryRequiredFields, + savedQueryOptionalFields, + {} +); +const patchSavedQueryParams = patchSchema(savedQueryRequiredFields, savedQueryOptionalFields, {}); +const responseSavedQueryParams = responseSchema( + savedQueryRequiredFields, + savedQueryOptionalFields, + {} +); + +const thresholdRequiredFields = { + type: t.literal('threshold'), + language, + query, + threshold, +}; + +const thresholdOptionalFields = { + index: indexOrUndefined, + filters: filtersOrUndefined, + saved_id: savedIdOrUndefined, +}; + +const createUpdateThresholdParams = createUpdateSchema( + thresholdRequiredFields, + thresholdOptionalFields, + {} +); +const patchThresholdParams = patchSchema(thresholdRequiredFields, thresholdOptionalFields, {}); +const responseThresholdParams = responseSchema( + thresholdRequiredFields, + thresholdOptionalFields, + {} +); + +const machineLearningRequiredFields = { + type: t.literal('machine_learning'), + anomaly_threshold, + machine_learning_job_id, +}; + +const machineLearningOptionalFields = {}; + +const createUpdateMachineLearningParams = createUpdateSchema( + machineLearningRequiredFields, + machineLearningOptionalFields, + {} +); +const patchMachineLearningParams = patchSchema( + machineLearningRequiredFields, + machineLearningOptionalFields, + {} +); +const responseMachineLearningParams = responseSchema( + machineLearningRequiredFields, + machineLearningOptionalFields, + {} +); + +const createUpdateTypeSpecific = t.union([ + createUpdateEqlParams, + createUpdateThreatMatchParams, + createUpdateQueryParams, + createUpdateSavedQueryParams, + createUpdateThresholdParams, + createUpdateMachineLearningParams, +]); +export type CreateUpdateTypeSpecific = t.TypeOf; + +const patchTypeSpecific = t.union([ + patchEqlParams, + patchThreatMatchParams, + patchQueryParams, + patchSavedQueryParams, + patchThresholdParams, + patchMachineLearningParams, +]); + +const responseTypeSpecific = t.union([ + responseEqlParams, + responseThreatMatchParams, + responseQueryParams, + responseSavedQueryParams, + responseThresholdParams, + responseMachineLearningParams, +]); + +const coreRequiredRuleFields = { + name, +}; + +const coreOptionalRuleFields = {}; + +const coreDefaultableRuleFields = { + tags, + interval, + enabled, + throttle: throttleOrNull, + actions, +}; + +const createUpdateCoreParams = createUpdateSchema( + coreRequiredRuleFields, + coreOptionalRuleFields, + coreDefaultableRuleFields +); +const patchCoreParams = patchSchema( + coreRequiredRuleFields, + coreOptionalRuleFields, + coreDefaultableRuleFields +); +const responseCoreParams = responseSchema( + coreRequiredRuleFields, + coreOptionalRuleFields, + coreDefaultableRuleFields +); + +export const fullCreateSchema = t.intersection([ + createUpdateCoreParams, + createUpdateRuleParams, + createUpdateTypeSpecific, +]); +export type FullCreateSchema = t.TypeOf; + +export const fullUpdateSchema = t.intersection([ + createUpdateCoreParams, + createUpdateRuleParams, + createUpdateTypeSpecific, + t.exact(t.partial({ id })), +]); + +export const fullPatchSchema = t.intersection([ + patchCoreParams, + patchRuleParams, + patchTypeSpecific, + t.exact(t.partial({ id })), +]); + +const responseRequiredFields = { + id, + immutable, + updated_at, + updated_by, + created_at, + created_by, +}; +const responseOptionalFields = { + status: job_status, + status_date, + last_success_at, + last_success_message, + last_failure_at, + last_failure_message, +}; + +export const fullResponseSchema = t.intersection([ + responseCoreParams, + responseRuleParams, + responseTypeSpecific, + t.exact(t.type(responseRequiredFields)), + t.exact(t.partial(responseOptionalFields)), +]); +export type FullResponseSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index 8076733be2d7d9..60cae6281478a1 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -22,11 +22,11 @@ import { getPrePackagedRulesStatus, } from './api'; import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock'; import { rulesMock } from './mock'; import { buildEsQuery } from 'src/plugins/data/common'; +import { getFullCreateSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; jest.mock('../../../../common/lib/kibana'); @@ -42,7 +42,7 @@ describe('Detections Rules API', () => { }); test('POSTs rule', async () => { - const payload = getCreateRulesSchemaMock(); + const payload = getFullCreateSchemaMock(); await createRule({ rule: payload, signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { body: diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 23adfe02283333..674bb4e3e18b5a 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { FullResponseSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; import { HttpStart } from '../../../../../../../../src/core/public'; import { DETECTION_ENGINE_RULES_URL, @@ -42,8 +43,8 @@ import { RulesSchema } from '../../../../../common/detection_engine/schemas/resp * * @throws An error if response is not OK */ -export const createRule = async ({ rule, signal }: CreateRulesProps): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { +export const createRule = async ({ rule, signal }: CreateRulesProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { method: 'POST', body: JSON.stringify(rule), signal, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index e9c89130736c05..6e4582e927e7ef 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -6,6 +6,7 @@ import * as t from 'io-ts'; +import { FullCreateSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; import { SortOrder, author, @@ -27,7 +28,6 @@ import { threat_filters, } from '../../../../../common/detection_engine/schemas/types'; import { - CreateRulesSchema, PatchRulesSchema, UpdateRulesSchema, } from '../../../../../common/detection_engine/schemas/request'; @@ -46,7 +46,7 @@ export const action = t.exact( ); export interface CreateRulesProps { - rule: CreateRulesSchema; + rule: FullCreateSchema; signal: AbortSignal; } diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx index 42d6a2a92a4c2f..223396f1dcf8db 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx @@ -7,7 +7,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useCreateRule, ReturnCreateRule } from './use_create_rule'; -import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; +import { getFullCreateSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('./api'); @@ -24,7 +24,7 @@ describe('useCreateRule', () => { useCreateRule() ); await waitForNextUpdate(); - result.current[1](getUpdateRulesSchemaMock()); + result.current[1](getFullCreateSchemaMock()); rerender(); expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); }); @@ -36,7 +36,7 @@ describe('useCreateRule', () => { useCreateRule() ); await waitForNextUpdate(); - result.current[1](getUpdateRulesSchemaMock()); + result.current[1](getFullCreateSchemaMock()); await waitForNextUpdate(); expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx index 2bbd27994fc771..fecf1dd3816b85 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx @@ -6,8 +6,8 @@ import { useEffect, useState, Dispatch } from 'react'; +import { FullCreateSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; -import { CreateRulesSchema } from '../../../../../common/detection_engine/schemas/request'; import { createRule } from './api'; import * as i18n from './translations'; @@ -17,10 +17,10 @@ interface CreateRuleReturn { isSaved: boolean; } -export type ReturnCreateRule = [CreateRuleReturn, Dispatch]; +export type ReturnCreateRule = [CreateRuleReturn, Dispatch]; export const useCreateRule = (): ReturnCreateRule => { - const [rule, setRule] = useState(null); + const [rule, setRule] = useState(null); const [isSaved, setIsSaved] = useState(false); const [isLoading, setIsLoading] = useState(false); const [, dispatchToaster] = useStateToaster(); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index 9d54879ee74953..04d5ac33c4b5bf 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -16,8 +16,8 @@ import React, { useCallback, useRef, useState, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import styled, { StyledComponent } from 'styled-components'; +import { FullCreateSchema } from '../../../../../../common/detection_engine/schemas/request/rule_schemas'; import { useCreateRule } from '../../../../containers/detection_engine/rules'; -import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; import { @@ -207,7 +207,7 @@ const CreateRulePageComponent: React.FC = () => { stepIsValid(actionsStep) ) { setRule( - formatRule( + formatRule( defineStep.data, aboutStep.data, scheduleStep.data, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 26febb0999ac70..9390f1ab1973e3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -160,9 +160,7 @@ describe('create_rules', () => { }); const result = server.validate(request); - expect(result.badRequest).toHaveBeenCalledWith( - 'Invalid value "unexpected_type" supplied to "type"' - ); + expect(result.badRequest).toHaveBeenCalled(); }); test('allows rule type of query and custom from and interval', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index 9940a56988c770..9d506d5806020e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -4,37 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqlRule } from '../../../../../common/detection_engine/utils'; -import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents'; -import { RuleAlertAction } from '../../../../../common/detection_engine/types'; +/* eslint-disable complexity */ + +import uuid from 'uuid'; + import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import { - createRulesSchema, - CreateRulesSchemaDecoded, -} from '../../../../../common/detection_engine/schemas/request/create_rules_schema'; import { IRouter } from '../../../../../../../../src/core/server'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { + DETECTION_ENGINE_RULES_URL, + SIGNALS_ID, + SERVER_APP_ID, + DEFAULT_MAX_SIGNALS, +} from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; -import { createRules } from '../../rules/create_rules'; import { readRules } from '../../rules/read_rules'; -import { transformValidate } from './validate'; import { getIndexExists } from '../../index/get_index_exists'; import { transformError, buildSiemResponse } from '../utils'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; -import { PartialFilter } from '../../types'; -import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { addTags } from '../../rules/add_tags'; +import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions'; +import { fullCreateSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; +import { newTransformValidate } from './validate'; +import { InternalRuleCreate } from '../../schemas/rule_schemas'; +import { typeSpecificSnakeToCamel } from '../../schemas/rule_converters'; export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void => { router.post( { path: DETECTION_ENGINE_RULES_URL, validate: { - body: buildRouteValidation( - createRulesSchema - ), + body: buildRouteValidation(fullCreateSchema), }, options: { tags: ['access:securitySolution'], @@ -42,68 +44,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - const validationErrors = createRuleValidateTypeDependents(request.body); - if (validationErrors.length) { - return siemResponse.error({ statusCode: 400, body: validationErrors }); - } - - const { - actions: actionsRest, - anomaly_threshold: anomalyThreshold, - author, - building_block_type: buildingBlockType, - description, - enabled, - event_category_override: eventCategoryOverride, - false_positives: falsePositives, - from, - query: queryOrUndefined, - language: languageOrUndefined, - license, - output_index: outputIndex, - saved_id: savedId, - timeline_id: timelineId, - timeline_title: timelineTitle, - meta, - machine_learning_job_id: machineLearningJobId, - filters: filtersRest, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - risk_score_mapping: riskScoreMapping, - rule_name_override: ruleNameOverride, - name, - severity, - severity_mapping: severityMapping, - tags, - threat, - threshold, - threat_filters: threatFilters, - threat_index: threatIndex, - threat_query: threatQuery, - threat_mapping: threatMapping, - threat_language: threatLanguage, - throttle, - timestamp_override: timestampOverride, - to, - type, - references, - note, - exceptions_list: exceptionsList, - } = request.body; try { - const query = !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined; - - const language = - !isMlRule(type) && !isEqlRule(type) && languageOrUndefined == null - ? 'kuery' - : languageOrUndefined; - - // 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 alertsClient = context.alerting?.getAlertsClient(); const clusterClient = context.core.elasticsearch.legacy.client; const savedObjectsClient = context.core.savedObjects.client; @@ -113,86 +54,92 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void return siemResponse.error({ statusCode: 404 }); } - const mlAuthz = buildMlAuthz({ license: context.licensing.license, ml, request }); - throwHttpError(await mlAuthz.validateRuleType(type)); - - const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); - const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); - if (!indexExists) { - return siemResponse.error({ - statusCode: 400, - body: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + if (request.body.rule_id != null) { + const rule = await readRules({ + alertsClient, + ruleId: request.body.rule_id, + id: undefined, }); - } - if (ruleId != null) { - const rule = await readRules({ alertsClient, ruleId, id: undefined }); if (rule != null) { return siemResponse.error({ statusCode: 409, - body: `rule_id: "${ruleId}" already exists`, + body: `rule_id: "${request.body.rule_id}" already exists`, }); } } + + const typeSpecificParams = typeSpecificSnakeToCamel(request.body); + const newRuleId = request.body.rule_id ?? uuid.v4(); + const throttle = request.body.throttle ?? null; + const internalRule: InternalRuleCreate = { + name: request.body.name, + tags: addTags(request.body.tags ?? [], newRuleId, false), + alertTypeId: SIGNALS_ID, + consumer: SERVER_APP_ID, + params: { + author: request.body.author ?? [], + buildingBlockType: request.body.building_block_type, + description: request.body.description, + ruleId: newRuleId, + falsePositives: request.body.false_positives ?? [], + from: request.body.from ?? 'now-6m', + immutable: false, + license: request.body.license, + outputIndex: request.body.output_index ?? siemClient.getSignalsIndex(), + timelineId: request.body.timeline_id, + timelineTitle: request.body.timeline_title, + meta: request.body.meta, + maxSignals: request.body.max_signals ?? DEFAULT_MAX_SIGNALS, + riskScore: request.body.risk_score, + riskScoreMapping: request.body.risk_score_mapping, + ruleNameOverride: request.body.rule_name_override, + severity: request.body.severity, + severityMapping: request.body.severity_mapping, + threat: request.body.threat ?? [], + timestampOverride: request.body.timestamp_override, + to: request.body.to ?? 'now', + references: request.body.references ?? [], + note: request.body.note, + version: request.body.version ?? 1, + exceptionsList: request.body.exceptions_list, + ...typeSpecificParams, + }, + schedule: { interval: request.body.interval ?? '5m' }, + enabled: request.body.enabled ?? true, + actions: + throttle === 'rule' ? (request.body.actions ?? []).map(transformRuleToAlertAction) : [], + throttle: null, + }; + + const mlAuthz = buildMlAuthz({ license: context.licensing.license, ml, request }); + throwHttpError(await mlAuthz.validateRuleType(internalRule.params.type)); + + const indexExists = await getIndexExists( + clusterClient.callAsCurrentUser, + internalRule.params.outputIndex + ); + if (!indexExists) { + return siemResponse.error({ + statusCode: 400, + body: `To create a rule, the index must exist first. Index ${internalRule.params.outputIndex} does not exist`, + }); + } + // This will create the endpoint list if it does not exist yet await context.lists?.getExceptionListClient().createEndpointList(); - const createdRule = await createRules({ - alertsClient, - anomalyThreshold, - author, - buildingBlockType, - description, - enabled, - eventCategoryOverride, - falsePositives, - from, - immutable: false, - query, - language, - license, - outputIndex: finalIndex, - savedId, - timelineId, - timelineTitle, - meta, - machineLearningJobId, - filters, - ruleId, - index, - interval, - maxSignals, - name, - riskScore, - riskScoreMapping, - ruleNameOverride, - severity, - severityMapping, - tags, - to, - type, - threat, - threshold, - threatFilters, - threatIndex, - threatQuery, - threatMapping, - threatLanguage, - timestampOverride, - references, - note, - version: 1, - exceptionsList, - actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is rule, otherwise we are a notification and should not enable it, + const createdRule = await alertsClient.create({ + data: internalRule, }); const ruleActions = await updateRulesNotifications({ ruleAlertId: createdRule.id, alertsClient, savedObjectsClient, - enabled, - actions, + enabled: createdRule.enabled, + actions: request.body.actions, throttle, - name, + name: createdRule.name, }); const ruleStatuses = await ruleStatusSavedObjectsClientFactory(savedObjectsClient).find({ @@ -202,7 +149,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void search: `${createdRule.id}`, searchFields: ['alertId'], }); - const [validated, errors] = transformValidate( + const [validated, errors] = newTransformValidate( createdRule, ruleActions, ruleStatuses.saved_objects[0] diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts index 983382b28ab381..3c2ab9bd3016de 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts @@ -9,6 +9,10 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; +import { + FullResponseSchema, + fullResponseSchema, +} from '../../../../../common/detection_engine/schemas/request/rule_schemas'; import { validate } from '../../../../../common/validate'; import { findRulesSchema } from '../../../../../common/detection_engine/schemas/response/find_rules_schema'; import { @@ -70,6 +74,19 @@ export const transformValidate = ( } }; +export const newTransformValidate = ( + alert: PartialAlert, + ruleActions?: RuleActions | null, + ruleStatus?: SavedObject +): [FullResponseSchema | null, string | null] => { + const transformed = transform(alert, ruleActions, ruleStatus); + if (transformed == null) { + return [null, 'Internal error transforming']; + } else { + return validate(transformed, fullResponseSchema); + } +}; + export const transformValidateBulkError = ( ruleId: string, alert: PartialAlert, 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 new file mode 100644 index 00000000000000..7f0b3091b1f7ed --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeSpecificRuleParams } from './rule_schemas'; +import { assertUnreachable } from '../../../../common/utility_types'; +import { CreateUpdateTypeSpecific } from '../../../../common/detection_engine/schemas/request/rule_schemas'; + +export const typeSpecificSnakeToCamel = ( + params: CreateUpdateTypeSpecific +): TypeSpecificRuleParams => { + switch (params.type) { + case 'eql': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + eventCategoryOverride: params.event_category_override, + }; + } + case 'threat_match': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + savedId: params.saved_id, + threatFilters: params.threat_filters, + threatQuery: params.threat_query, + threatMapping: params.threat_mapping, + threatLanguage: params.threat_language, + }; + } + case 'query': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + savedId: params.saved_id, + }; + } + case 'saved_query': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + savedId: params.saved_id, + }; + } + case 'threshold': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + savedId: params.saved_id, + threshold: params.threshold, + }; + } + case 'machine_learning': { + return { + type: params.type, + anomalyThreshold: params.anomaly_threshold, + machineLearningJobId: params.machine_learning_job_id, + }; + } + default: { + return assertUnreachable(params); + } + } +}; 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 new file mode 100644 index 00000000000000..a50bb5905c6aef --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts @@ -0,0 +1,178 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +import { listArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists'; +import { + threatQueryOrUndefined, + threatMappingOrUndefined, + threatLanguageOrUndefined, +} from '../../../../common/detection_engine/schemas/types/threat_mapping'; +import { + author, + buildingBlockTypeOrUndefined, + description, + enabled, + noteOrUndefined, + false_positives, + from, + rule_id, + immutable, + indexOrUndefined, + language, + languageOrUndefined, + licenseOrUndefined, + output_index, + timelineIdOrUndefined, + timelineTitleOrUndefined, + metaOrUndefined, + name, + query, + queryOrUndefined, + filtersOrUndefined, + machine_learning_job_id, + max_signals, + risk_score, + riskScoreMappingOrUndefined, + ruleNameOverrideOrUndefined, + severity, + severityMappingOrUndefined, + tags, + timestampOverrideOrUndefined, + threat, + to, + references, + version, + eventCategoryOverrideOrUndefined, + savedIdOrUndefined, + saved_id, + threshold, + anomaly_threshold, + actionsCamel, + throttleOrNull, +} from '../../../../common/detection_engine/schemas/common/schemas'; +import { SIGNALS_ID, SERVER_APP_ID } from '../../../../common/constants'; + +export const baseRuleParams = t.exact( + t.type({ + author, + buildingBlockType: buildingBlockTypeOrUndefined, + description, + note: noteOrUndefined, + falsePositives: false_positives, + from, + ruleId: rule_id, + immutable, + license: licenseOrUndefined, + outputIndex: output_index, + timelineId: timelineIdOrUndefined, + timelineTitle: timelineTitleOrUndefined, + meta: metaOrUndefined, + // maxSignals not used in ML rules but probably should be used + maxSignals: max_signals, + riskScore: risk_score, + riskScoreMapping: riskScoreMappingOrUndefined, + ruleNameOverride: ruleNameOverrideOrUndefined, + severity, + severityMapping: severityMappingOrUndefined, + timestampOverride: timestampOverrideOrUndefined, + threat, + to, + references, + version, + exceptionsList: listArrayOrUndefined, + }) +); +export type BaseRuleParams = t.TypeOf; + +const eqlSpecificRuleParams = t.type({ + type: t.literal('eql'), + language: t.literal('eql'), + index: indexOrUndefined, + query, + filters: filtersOrUndefined, + eventCategoryOverride: eventCategoryOverrideOrUndefined, +}); + +const threatSpecificRuleParams = t.type({ + type: t.literal('threat_match'), + language, + index: indexOrUndefined, + query, + filters: filtersOrUndefined, + savedId: savedIdOrUndefined, + threatFilters: filtersOrUndefined, + threatQuery: threatQueryOrUndefined, + threatMapping: threatMappingOrUndefined, + threatLanguage: threatLanguageOrUndefined, +}); + +const querySpecificRuleParams = t.exact( + t.type({ + type: t.literal('query'), + language, + index: indexOrUndefined, + query, + filters: filtersOrUndefined, + savedId: savedIdOrUndefined, + }) +); + +const savedQuerySpecificRuleParams = t.type({ + type: t.literal('saved_query'), + // Having language, query, and filters possibly defined adds more code confusion and probably user confusion + // if the saved object gets deleted for some reason + language: languageOrUndefined, + index: indexOrUndefined, + query: queryOrUndefined, + filters: filtersOrUndefined, + savedId: saved_id, +}); + +const thresholdSpecificRuleParams = t.type({ + type: t.literal('threshold'), + language, + index: indexOrUndefined, + query, + filters: filtersOrUndefined, + savedId: savedIdOrUndefined, + threshold, +}); + +const machineLearningSpecificRuleParams = t.type({ + type: t.literal('machine_learning'), + anomalyThreshold: anomaly_threshold, + machineLearningJobId: machine_learning_job_id, +}); + +export const typeSpecificRuleParams = t.union([ + eqlSpecificRuleParams, + threatSpecificRuleParams, + querySpecificRuleParams, + savedQuerySpecificRuleParams, + thresholdSpecificRuleParams, + machineLearningSpecificRuleParams, +]); +export type TypeSpecificRuleParams = t.TypeOf; + +export const ruleParams = t.intersection([baseRuleParams, typeSpecificRuleParams]); +export type RuleParams = t.TypeOf; + +export const internalRuleCreate = t.type({ + name, + tags, + alertTypeId: t.literal(SIGNALS_ID), + consumer: t.literal(SERVER_APP_ID), + schedule: t.type({ + interval: t.string, + }), + enabled, + actions: actionsCamel, + params: ruleParams, + throttle: throttleOrNull, +}); +export type InternalRuleCreate = t.TypeOf; From 2ca8b1f1eb651953b487bc075389d7eeb19f2361 Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Wed, 4 Nov 2020 11:48:47 -0500 Subject: [PATCH 02/19] Update integration tests with new schema --- .../schemas/request/rule_schemas.mock.ts | 42 +++++++++++ .../schemas/request/rule_schemas.ts | 74 +++++++++++++------ .../schemas/rule_converters.ts | 7 +- .../detection_engine/schemas/rule_schemas.ts | 24 +++--- .../basic/tests/create_rules.ts | 45 ++++++++++- .../basic/tests/update_rules.ts | 13 ++-- .../basic/tests/update_rules_bulk.ts | 21 +++--- .../security_and_spaces/tests/add_actions.ts | 4 +- .../tests/create_exceptions.ts | 30 ++++++-- .../security_and_spaces/tests/create_rules.ts | 45 ++++++++++- .../tests/create_threat_matching.ts | 50 +++++++++---- .../security_and_spaces/tests/update_rules.ts | 11 +-- .../tests/update_rules_bulk.ts | 21 +++--- .../detection_engine_api_integration/utils.ts | 14 ++-- 14 files changed, 298 insertions(+), 103 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts index e13b7cd46245ab..ae71c0cbafc3b6 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts @@ -16,3 +16,45 @@ export const getFullCreateSchemaMock = (ruleId = 'rule-1'): FullCreateSchema => language: 'kuery', rule_id: ruleId, }); + +export const getFullCreateThreatMatchSchemaMock = (ruleId = 'rule-1'): FullCreateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: ruleId, + threat_query: '*:*', + threat_index: ['list-index'], + threat_mapping: [ + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [ + { + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }, + ], +}); 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 9c01f70ec7f10f..a79aa838bb0ca9 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 @@ -11,7 +11,7 @@ import { threat_filters, threat_query, threat_mapping, - threat_language, + threat_index, } from '../types/threat_mapping'; import { id, @@ -36,7 +36,6 @@ import { rule_id, immutable, indexOrUndefined, - language, output_index, query, filtersOrUndefined, @@ -186,8 +185,10 @@ const responseEqlParams = responseSchema(eqlRequiredFields, eqlOptionalFields, { const threatMatchRequiredFields = { type: t.literal('threat_match'), - language: t.keyof({ kuery: null, lucene: null }), query, + threat_query, + threat_mapping, + threat_index, }; const threatMatchOptionalFields = { @@ -195,30 +196,31 @@ const threatMatchOptionalFields = { filters, saved_id, threat_filters, - threat_query, - threat_mapping, - threat_language, + threat_language: t.keyof({ kuery: null, lucene: null }), +}; + +const threatMatchDefaultableFields = { + language: t.keyof({ kuery: null, lucene: null }), }; const createUpdateThreatMatchParams = createUpdateSchema( threatMatchRequiredFields, threatMatchOptionalFields, - {} + threatMatchDefaultableFields ); const patchThreatMatchParams = patchSchema( threatMatchRequiredFields, threatMatchOptionalFields, - {} + threatMatchDefaultableFields ); const responseThreatMatchParams = responseSchema( threatMatchRequiredFields, threatMatchOptionalFields, - {} + threatMatchDefaultableFields ); const queryRequiredFields = { type: t.literal('query'), - language: t.keyof({ kuery: null, lucene: null }), query, }; @@ -228,9 +230,25 @@ const queryOptionalFields = { saved_id, }; -const createUpdateQueryParams = createUpdateSchema(queryRequiredFields, queryOptionalFields, {}); -const patchQueryParams = patchSchema(queryRequiredFields, queryOptionalFields, {}); -const responseQueryParams = responseSchema(queryRequiredFields, queryOptionalFields, {}); +const queryDefaultableFields = { + language: t.keyof({ kuery: null, lucene: null }), +}; + +const createUpdateQueryParams = createUpdateSchema( + queryRequiredFields, + queryOptionalFields, + queryDefaultableFields +); +const patchQueryParams = patchSchema( + queryRequiredFields, + queryOptionalFields, + queryDefaultableFields +); +const responseQueryParams = responseSchema( + queryRequiredFields, + queryOptionalFields, + queryDefaultableFields +); const savedQueryRequiredFields = { type: t.literal('saved_query'), @@ -240,27 +258,33 @@ const savedQueryRequiredFields = { const savedQueryOptionalFields = { // Having language, query, and filters possibly defined adds more code confusion and probably user confusion // if the saved object gets deleted for some reason - language, index, query, filters, }; +const savedQueryDefaultableFields = { + language: t.keyof({ kuery: null, lucene: null }), +}; + const createUpdateSavedQueryParams = createUpdateSchema( savedQueryRequiredFields, savedQueryOptionalFields, - {} + savedQueryDefaultableFields +); +const patchSavedQueryParams = patchSchema( + savedQueryRequiredFields, + savedQueryOptionalFields, + savedQueryDefaultableFields ); -const patchSavedQueryParams = patchSchema(savedQueryRequiredFields, savedQueryOptionalFields, {}); const responseSavedQueryParams = responseSchema( savedQueryRequiredFields, savedQueryOptionalFields, - {} + savedQueryDefaultableFields ); const thresholdRequiredFields = { type: t.literal('threshold'), - language, query, threshold, }; @@ -271,16 +295,24 @@ const thresholdOptionalFields = { saved_id: savedIdOrUndefined, }; +const thresholdDefaultableFields = { + language: t.keyof({ kuery: null, lucene: null }), +}; + const createUpdateThresholdParams = createUpdateSchema( thresholdRequiredFields, thresholdOptionalFields, - {} + thresholdDefaultableFields +); +const patchThresholdParams = patchSchema( + thresholdRequiredFields, + thresholdOptionalFields, + thresholdDefaultableFields ); -const patchThresholdParams = patchSchema(thresholdRequiredFields, thresholdOptionalFields, {}); const responseThresholdParams = responseSchema( thresholdRequiredFields, thresholdOptionalFields, - {} + thresholdDefaultableFields ); const machineLearningRequiredFields = { 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 7f0b3091b1f7ed..512fc2bb31d898 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 @@ -25,7 +25,7 @@ export const typeSpecificSnakeToCamel = ( case 'threat_match': { return { type: params.type, - language: params.language, + language: params.language ?? 'kuery', index: params.index, query: params.query, filters: params.filters, @@ -34,12 +34,13 @@ export const typeSpecificSnakeToCamel = ( threatQuery: params.threat_query, threatMapping: params.threat_mapping, threatLanguage: params.threat_language, + threatIndex: params.threat_index, }; } case 'query': { return { type: params.type, - language: params.language, + language: params.language ?? 'kuery', index: params.index, query: params.query, filters: params.filters, @@ -59,7 +60,7 @@ export const typeSpecificSnakeToCamel = ( case 'threshold': { return { type: params.type, - language: params.language, + language: params.language ?? 'kuery', index: params.index, query: params.query, filters: params.filters, 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 a50bb5905c6aef..85663b0a74c934 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 @@ -8,9 +8,9 @@ import * as t from 'io-ts'; import { listArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists'; import { - threatQueryOrUndefined, - threatMappingOrUndefined, - threatLanguageOrUndefined, + threat_mapping, + threat_index, + threat_query, } from '../../../../common/detection_engine/schemas/types/threat_mapping'; import { author, @@ -23,8 +23,6 @@ import { rule_id, immutable, indexOrUndefined, - language, - languageOrUndefined, licenseOrUndefined, output_index, timelineIdOrUndefined, @@ -57,6 +55,7 @@ import { } from '../../../../common/detection_engine/schemas/common/schemas'; import { SIGNALS_ID, SERVER_APP_ID } from '../../../../common/constants'; +const nonEqlLanguages = t.keyof({ kuery: null, lucene: null }); export const baseRuleParams = t.exact( t.type({ author, @@ -100,21 +99,22 @@ const eqlSpecificRuleParams = t.type({ const threatSpecificRuleParams = t.type({ type: t.literal('threat_match'), - language, + language: nonEqlLanguages, index: indexOrUndefined, query, filters: filtersOrUndefined, savedId: savedIdOrUndefined, threatFilters: filtersOrUndefined, - threatQuery: threatQueryOrUndefined, - threatMapping: threatMappingOrUndefined, - threatLanguage: threatLanguageOrUndefined, + threatQuery: threat_query, + threatMapping: threat_mapping, + threatLanguage: t.union([nonEqlLanguages, t.undefined]), + threatIndex: threat_index, }); const querySpecificRuleParams = t.exact( t.type({ type: t.literal('query'), - language, + language: nonEqlLanguages, index: indexOrUndefined, query, filters: filtersOrUndefined, @@ -126,7 +126,7 @@ const savedQuerySpecificRuleParams = t.type({ type: t.literal('saved_query'), // Having language, query, and filters possibly defined adds more code confusion and probably user confusion // if the saved object gets deleted for some reason - language: languageOrUndefined, + language: t.union([nonEqlLanguages, t.undefined]), index: indexOrUndefined, query: queryOrUndefined, filters: filtersOrUndefined, @@ -135,7 +135,7 @@ const savedQuerySpecificRuleParams = t.type({ const thresholdSpecificRuleParams = t.type({ type: t.literal('threshold'), - language, + language: nonEqlLanguages, index: indexOrUndefined, query, filters: filtersOrUndefined, diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts index 237feb84932d21..fda2ebf9e74416 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { FullCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -65,13 +66,51 @@ export default ({ getService }: FtrProviderContext) => { }); it('should create a single rule without an input index', async () => { - const { index, ...payload } = getSimpleRule(); - const { index: _index, ...expected } = getSimpleRuleOutput(); + const rule: FullCreateSchema = { + name: 'Simple Rule Query', + description: 'Simple Rule Query', + enabled: true, + risk_score: 1, + rule_id: 'rule-1', + severity: 'high', + type: 'query', + query: 'user.name: root or user.name: admin', + }; + const expected = { + actions: [], + author: [], + created_by: 'elastic', + description: 'Simple Rule Query', + enabled: true, + false_positives: [], + from: 'now-6m', + immutable: false, + interval: '5m', + rule_id: 'rule-1', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 1, + risk_score_mapping: [], + name: 'Simple Rule Query', + query: 'user.name: root or user.name: admin', + references: [], + severity: 'high', + severity_mapping: [], + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + threat: [], + throttle: 'no_actions', + exceptions_list: [], + version: 1, + }; const { body } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(payload) + .send(rule) .expect(200); const bodyToCompare = removeServerGeneratedProperties(body); diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts index a0fcac159a73ee..2f5a043881eeb0 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules.ts @@ -19,6 +19,7 @@ import { getSimpleRuleUpdate, getSimpleMlRuleUpdate, createRule, + getSimpleRule, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -38,7 +39,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using a rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule = getSimpleRuleUpdate('rule-1'); @@ -60,7 +61,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return a 403 forbidden if it is a machine learning job', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's type to try to be a machine learning job type const updatedRule = getSimpleMlRuleUpdate('rule-1'); @@ -81,7 +82,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using an auto-generated rule_id', async () => { - const rule = getSimpleRuleUpdate('rule-1'); + const rule = getSimpleRule('rule-1'); delete rule.rule_id; const createRuleBody = await createRule(supertest, rule); @@ -105,7 +106,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using the auto-generated id', async () => { - const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createdBody = await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule = getSimpleRuleUpdate('rule-1'); @@ -127,7 +128,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change the version of a rule when it updates enabled and another property', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's enabled to false and another property const updatedRule = getSimpleRuleUpdate('rule-1'); @@ -150,7 +151,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.timeline_title = 'some title'; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts index a846771fe76837..22aa40b0721a43 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/update_rules_bulk.ts @@ -18,6 +18,7 @@ import { removeServerGeneratedPropertiesIncludingRuleId, getSimpleRuleUpdate, createRule, + getSimpleRule, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -37,7 +38,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using a rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); const updatedRule = getSimpleRuleUpdate('rule-1'); updatedRule.name = 'some other name'; @@ -57,7 +58,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update two rule properties of name using the two rules rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // create a second simple rule await supertest @@ -94,7 +95,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using an id', async () => { - const createRuleBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createRuleBody = await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -116,8 +117,8 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update two rule properties of name using the two rules id', async () => { - const createRule1 = await createRule(supertest, getSimpleRuleUpdate('rule-1')); - const createRule2 = await createRule(supertest, getSimpleRuleUpdate('rule-2')); + const createRule1 = await createRule(supertest, getSimpleRule('rule-1')); + const createRule2 = await createRule(supertest, getSimpleRule('rule-2')); // update both rule names const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -151,7 +152,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using the auto-generated id', async () => { - const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createdBody = await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -173,7 +174,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change the version of a rule when it updates enabled and another property', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's enabled to false and another property const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -196,7 +197,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's timeline_title const ruleUpdate = getSimpleRuleUpdate('rule-1'); @@ -269,7 +270,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update one rule property and give an error about a second fake rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.name = 'some other name'; @@ -304,7 +305,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update one rule property and give an error about a second fake id', async () => { - const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createdBody = await createRule(supertest, getSimpleRule('rule-1')); // update one rule name and give a fake id for the second const rule1 = getSimpleRuleUpdate(); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts index 75d5e47b7b990b..fd12a5ac02d612 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; +import { FullCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { @@ -19,7 +20,6 @@ import { waitForRuleSuccess, createRule, } from '../../utils'; -import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -81,7 +81,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); // create a rule with the action attached and a meta field - const ruleWithAction: CreateRulesSchema = { + const ruleWithAction: FullCreateSchema = { ...getRuleWithWebHookAction(hookAction.id), meta: {}, }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts index ae4ff41c509eae..0d0437826fbc01 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts @@ -7,10 +7,10 @@ /* eslint-disable @typescript-eslint/naming-convention */ import expect from '@kbn/expect'; +import { FullCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; import { deleteAllExceptions } from '../../../lists_api_integration/utils'; import { RulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/response'; -import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; import { CreateExceptionListItemSchema } from '../../../../plugins/lists/common'; import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; @@ -62,7 +62,7 @@ export default ({ getService }: FtrProviderContext) => { .send(getCreateExceptionListMinimalSchemaMock()) .expect(200); - const ruleWithException: CreateRulesSchema = { + const ruleWithException: FullCreateSchema = { ...getSimpleRule(), exceptions_list: [ { @@ -99,7 +99,7 @@ export default ({ getService }: FtrProviderContext) => { .send(getCreateExceptionListMinimalSchemaMock()) .expect(200); - const ruleWithException: CreateRulesSchema = { + const ruleWithException: FullCreateSchema = { ...getSimpleRule(), exceptions_list: [ { @@ -421,8 +421,15 @@ export default ({ getService }: FtrProviderContext) => { }; await createExceptionListItem(supertest, exceptionListItem); - const ruleWithException: CreateRulesSchema = { - ...getSimpleRule(), + const ruleWithException: FullCreateSchema = { + name: 'Simple Rule Query', + description: 'Simple Rule Query', + enabled: true, + risk_score: 1, + rule_id: 'rule-1', + severity: 'high', + index: ['auditbeat-*'], + type: 'query', from: '1900-01-01T00:00:00.000Z', query: 'host.name: "suricata-sensor-amsterdam"', exceptions_list: [ @@ -459,10 +466,17 @@ export default ({ getService }: FtrProviderContext) => { }; await createExceptionListItem(supertest, exceptionListItem); - const ruleWithException: CreateRulesSchema = { - ...getSimpleRule(), + const ruleWithException: FullCreateSchema = { + name: 'Simple Rule Query', + description: 'Simple Rule Query', + enabled: true, + risk_score: 1, + rule_id: 'rule-1', + severity: 'high', + index: ['auditbeat-*'], + type: 'query', from: '1900-01-01T00:00:00.000Z', - query: 'host.name: "suricata-sensor-amsterdam"', // this matches all the exceptions we should exclude + query: 'host.name: "suricata-sensor-amsterdam"', exceptions_list: [ { id, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index 4d9bff8b0f34e5..aee768bc3125ea 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { FullCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { DETECTION_ENGINE_RULES_URL, @@ -110,13 +111,51 @@ export default ({ getService }: FtrProviderContext) => { }); it('should create a single rule without an input index', async () => { - const { index, ...payload } = getSimpleRule(); - const { index: _index, ...expected } = getSimpleRuleOutput(); + const rule: FullCreateSchema = { + name: 'Simple Rule Query', + description: 'Simple Rule Query', + enabled: true, + risk_score: 1, + rule_id: 'rule-1', + severity: 'high', + type: 'query', + query: 'user.name: root or user.name: admin', + }; + const expected = { + actions: [], + author: [], + created_by: 'elastic', + description: 'Simple Rule Query', + enabled: true, + false_positives: [], + from: 'now-6m', + immutable: false, + interval: '5m', + rule_id: 'rule-1', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 1, + risk_score_mapping: [], + name: 'Simple Rule Query', + query: 'user.name: root or user.name: admin', + references: [], + severity: 'high', + severity_mapping: [], + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + threat: [], + throttle: 'no_actions', + exceptions_list: [], + version: 1, + }; const { body } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(payload) + .send(rule) .expect(200); const bodyToCompare = removeServerGeneratedProperties(body); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index 498c6071217602..ca00ce06ffdf2c 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { FullCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_RULES_STATUS_URL, @@ -23,7 +23,7 @@ import { waitForSignalsToBePresent, } from '../../utils'; -import { getCreateThreatMatchRulesSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getFullCreateThreatMatchSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock'; import { getThreatMatchingSchemaPartialMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks'; // eslint-disable-next-line import/no-default-export @@ -41,7 +41,7 @@ export default ({ getService }: FtrProviderContext) => { const { body } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getCreateThreatMatchRulesSchemaMock()) + .send(getFullCreateThreatMatchSchemaMock()) .expect(400); expect(body).to.eql({ @@ -63,13 +63,13 @@ export default ({ getService }: FtrProviderContext) => { }); it('should create a single rule with a rule_id', async () => { - const ruleResponse = await createRule(supertest, getCreateThreatMatchRulesSchemaMock()); + const ruleResponse = await createRule(supertest, getFullCreateThreatMatchSchemaMock()); const bodyToCompare = removeServerGeneratedProperties(ruleResponse); expect(bodyToCompare).to.eql(getThreatMatchingSchemaPartialMock()); }); it('should create a single rule with a rule_id and validate it ran successfully', async () => { - const ruleResponse = await createRule(supertest, getCreateThreatMatchRulesSchemaMock()); + const ruleResponse = await createRule(supertest, getFullCreateThreatMatchSchemaMock()); await waitForRuleSuccess(supertest, ruleResponse.id); const { body: statusBody } = await supertest @@ -98,8 +98,14 @@ export default ({ getService }: FtrProviderContext) => { }); it('should be able to execute and get 10 signals when doing a specific query', async () => { - const rule: CreateRulesSchema = { - ...getCreateThreatMatchRulesSchemaMock(), + const rule: FullCreateSchema = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', from: '1900-01-01T00:00:00.000Z', query: '*:*', threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip @@ -126,8 +132,14 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return 0 matches if the mapping does not match against anything in the mapping', async () => { - const rule: CreateRulesSchema = { - ...getCreateThreatMatchRulesSchemaMock(), + const rule: FullCreateSchema = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', from: '1900-01-01T00:00:00.000Z', query: '*:*', threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip @@ -154,8 +166,14 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return 0 signals when using an AND and one of the clauses does not have data', async () => { - const rule: CreateRulesSchema = { - ...getCreateThreatMatchRulesSchemaMock(), + const rule: FullCreateSchema = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', from: '1900-01-01T00:00:00.000Z', query: '*:*', threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip @@ -186,8 +204,14 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return 0 signals when using an AND and one of the clauses has a made up value that does not exist', async () => { - const rule: CreateRulesSchema = { - ...getCreateThreatMatchRulesSchemaMock(), + const rule: FullCreateSchema = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', from: '1900-01-01T00:00:00.000Z', query: '*:*', threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts index 3150e7c5b71c76..23a8776b14631d 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts @@ -21,6 +21,7 @@ import { getSimpleRuleUpdate, getSimpleMlRuleUpdate, createRule, + getSimpleRule, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -40,7 +41,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using a rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule = getSimpleRuleUpdate('rule-1'); @@ -84,7 +85,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using an auto-generated rule_id', async () => { - const rule = getSimpleRuleUpdate('rule-1'); + const rule = getSimpleRule('rule-1'); delete rule.rule_id; const createRuleBody = await createRule(supertest, rule); @@ -108,7 +109,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using the auto-generated id', async () => { - const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createdBody = await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule = getSimpleRuleUpdate('rule-1'); @@ -130,7 +131,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change the version of a rule when it updates enabled and another property', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's enabled to false and another property const updatedRule = getSimpleRuleUpdate('rule-1'); @@ -153,7 +154,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.timeline_title = 'some title'; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts index a846771fe76837..22aa40b0721a43 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts @@ -18,6 +18,7 @@ import { removeServerGeneratedPropertiesIncludingRuleId, getSimpleRuleUpdate, createRule, + getSimpleRule, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -37,7 +38,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using a rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); const updatedRule = getSimpleRuleUpdate('rule-1'); updatedRule.name = 'some other name'; @@ -57,7 +58,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update two rule properties of name using the two rules rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // create a second simple rule await supertest @@ -94,7 +95,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using an id', async () => { - const createRuleBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createRuleBody = await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -116,8 +117,8 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update two rule properties of name using the two rules id', async () => { - const createRule1 = await createRule(supertest, getSimpleRuleUpdate('rule-1')); - const createRule2 = await createRule(supertest, getSimpleRuleUpdate('rule-2')); + const createRule1 = await createRule(supertest, getSimpleRule('rule-1')); + const createRule2 = await createRule(supertest, getSimpleRule('rule-2')); // update both rule names const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -151,7 +152,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update a single rule property of name using the auto-generated id', async () => { - const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createdBody = await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's name const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -173,7 +174,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change the version of a rule when it updates enabled and another property', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's enabled to false and another property const updatedRule1 = getSimpleRuleUpdate('rule-1'); @@ -196,7 +197,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); // update a simple rule's timeline_title const ruleUpdate = getSimpleRuleUpdate('rule-1'); @@ -269,7 +270,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update one rule property and give an error about a second fake rule_id', async () => { - await createRule(supertest, getSimpleRuleUpdate('rule-1')); + await createRule(supertest, getSimpleRule('rule-1')); const ruleUpdate = getSimpleRuleUpdate('rule-1'); ruleUpdate.name = 'some other name'; @@ -304,7 +305,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should update one rule property and give an error about a second fake id', async () => { - const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1')); + const createdBody = await createRule(supertest, getSimpleRule('rule-1')); // update one rule name and give a fake id for the second const rule1 = getSimpleRuleUpdate(); diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 05a0f73dd0dc4f..6ee67dfe08a723 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -9,6 +9,7 @@ import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; import { Context } from '@elastic/elasticsearch/lib/Transport'; import { SearchResponse } from 'elasticsearch'; +import { FullCreateSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../../plugins/lists/common/constants'; import { CreateExceptionListItemSchema, @@ -21,7 +22,6 @@ import { Status, SignalIds, } from '../../plugins/security_solution/common/detection_engine/schemas/common/schemas'; -import { CreateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema'; import { UpdateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema'; import { RulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema'; import { @@ -74,7 +74,7 @@ export const removeServerGeneratedPropertiesIncludingRuleId = ( * @param ruleId * @param enabled Enables the rule on creation or not. Defaulted to false to enable it on import */ -export const getSimpleRule = (ruleId = 'rule-1', enabled = true): CreateRulesSchema => ({ +export const getSimpleRule = (ruleId = 'rule-1', enabled = true): FullCreateSchema => ({ name: 'Simple Rule Query', description: 'Simple Rule Query', enabled, @@ -105,7 +105,7 @@ export const getSimpleRuleUpdate = (ruleId = 'rule-1'): UpdateRulesSchema => ({ * This is a representative ML rule payload as expected by the server * @param ruleId */ -export const getSimpleMlRule = (ruleId = 'rule-1'): CreateRulesSchema => ({ +export const getSimpleMlRule = (ruleId = 'rule-1'): FullCreateSchema => ({ name: 'Simple ML Rule', description: 'Simple Machine Learning Rule', anomaly_threshold: 44, @@ -172,7 +172,7 @@ export const getSignalStatusEmptyResponse = () => ({ /** * This is a typical simple rule for testing that is easy for most basic testing */ -export const getSimpleRuleWithoutRuleId = (): CreateRulesSchema => { +export const getSimpleRuleWithoutRuleId = (): FullCreateSchema => { const simpleRule = getSimpleRule(); // eslint-disable-next-line @typescript-eslint/naming-convention const { rule_id, ...ruleWithoutId } = simpleRule; @@ -371,7 +371,7 @@ export const getSimpleRuleAsNdjson = (ruleIds: string[], enabled = false): Buffe * testing upload features. * @param rule The rule to convert to ndjson */ -export const ruleToNdjson = (rule: Partial): Buffer => { +export const ruleToNdjson = (rule: FullCreateSchema): Buffer => { const stringified = JSON.stringify(rule); return Buffer.from(`${stringified}\n`); }; @@ -568,7 +568,7 @@ export const getWebHookAction = () => ({ name: 'Some connector', }); -export const getRuleWithWebHookAction = (id: string): CreateRulesSchema => ({ +export const getRuleWithWebHookAction = (id: string): FullCreateSchema => ({ ...getSimpleRule(), throttle: 'rule', actions: [ @@ -711,7 +711,7 @@ export const countDownTest = async ( */ export const createRule = async ( supertest: SuperTest, - rule: CreateRulesSchema + rule: FullCreateSchema ): Promise => { const { body } = await supertest .post(DETECTION_ENGINE_RULES_URL) From 0166475e8252655644e5a543e357fd34d3677434 Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Wed, 4 Nov 2020 12:01:34 -0500 Subject: [PATCH 03/19] Fix response type on createRule helper --- .../detection_engine_api_integration/utils.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 6ee67dfe08a723..135df824966f42 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -9,7 +9,10 @@ import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; import { Context } from '@elastic/elasticsearch/lib/Transport'; import { SearchResponse } from 'elasticsearch'; -import { FullCreateSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; +import { + FullCreateSchema, + FullResponseSchema, +} from '../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../../plugins/lists/common/constants'; import { CreateExceptionListItemSchema, @@ -37,8 +40,8 @@ import { * @param rule Rule to pass in to remove typical server generated properties */ export const removeServerGeneratedProperties = ( - rule: Partial -): Partial => { + rule: FullResponseSchema +): Partial => { const { /* eslint-disable @typescript-eslint/naming-convention */ created_at, @@ -61,8 +64,8 @@ export const removeServerGeneratedProperties = ( * @param rule Rule to pass in to remove typical server generated properties */ export const removeServerGeneratedPropertiesIncludingRuleId = ( - rule: Partial -): Partial => { + rule: FullResponseSchema +): Partial => { const ruleWithRemovedProperties = removeServerGeneratedProperties(rule); // eslint-disable-next-line @typescript-eslint/naming-convention const { rule_id, ...additionalRuledIdRemoved } = ruleWithRemovedProperties; @@ -712,7 +715,7 @@ export const countDownTest = async ( export const createRule = async ( supertest: SuperTest, rule: FullCreateSchema -): Promise => { +): Promise => { const { body } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') From e3b4449176508875571c0d4d223069c7985bfbba Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Thu, 5 Nov 2020 18:31:36 -0500 Subject: [PATCH 04/19] Add unit tests for new rule schema, add defaults for some array fields, clean up API schema definitions --- .../schemas/common/schemas.ts | 4 + .../schemas/request/rule_schemas.mock.ts | 18 +- .../schemas/request/rule_schemas.test.ts | 1129 +++++++++++++++++ .../schemas/request/rule_schemas.ts | 507 ++++---- .../routes/rules/create_rules_route.ts | 8 +- .../schemas/rule_converters.ts | 142 ++- .../detection_engine/schemas/rule_schemas.ts | 30 +- 7 files changed, 1538 insertions(+), 300 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index ac7a31f350e85b..82b803c62a9406 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -449,6 +449,10 @@ export const created_at = IsoDateString; export const updated_at = IsoDateString; export const updated_by = t.string; export const created_by = t.string; +export const updatedByOrNull = t.union([updated_by, t.null]); +export type UpdatedByOrNull = t.TypeOf; +export const createdByOrNull = t.union([created_by, t.null]); +export type CreatedByOrNull = t.TypeOf; export const version = PositiveIntegerGreaterThanZero; export type Version = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts index ae71c0cbafc3b6..2670edd95c6fb6 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FullCreateSchema } from './rule_schemas'; +import { QueryCreateSchema, SavedQueryCreateSchema, ThreatMatchCreateSchema } from './rule_schemas'; -export const getFullCreateSchemaMock = (ruleId = 'rule-1'): FullCreateSchema => ({ +export const getFullCreateSchemaMock = (ruleId = 'rule-1'): QueryCreateSchema => ({ description: 'Detecting root and admin users', name: 'Query with a rule id', query: 'user.name: root or user.name: admin', @@ -17,7 +17,19 @@ export const getFullCreateSchemaMock = (ruleId = 'rule-1'): FullCreateSchema => rule_id: ruleId, }); -export const getFullCreateThreatMatchSchemaMock = (ruleId = 'rule-1'): FullCreateSchema => ({ +export const getCreateSavedQuerySchemaMock = (ruleId = 'rule-1'): SavedQueryCreateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'saved_query', + saved_id: 'some id', + risk_score: 55, + language: 'kuery', + rule_id: ruleId, +}); + +export const getCreateThreatMatchSchemaMock = (ruleId = 'rule-1'): ThreatMatchCreateSchema => ({ description: 'Detecting root and admin users', name: 'Query with a rule id', query: 'user.name: root or user.name: admin', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts new file mode 100644 index 00000000000000..5e3092dd01b78d --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts @@ -0,0 +1,1129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fullCreateSchema, FullCreateSchema, SavedQueryCreateSchema } from './rule_schemas'; +import { exactCheck } from '../../../exact_check'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { foldLeftRight, getPaths } from '../../../test_utils'; +import { left } from 'fp-ts/lib/Either'; +import { + getCreateSavedQuerySchemaMock, + getCreateThreatMatchSchemaMock, + getFullCreateSchemaMock, +} from './rule_schemas.mock'; +import { DEFAULT_MAX_SIGNALS } from '../../../constants'; +import { getListArrayMock } from '../types/lists.mock'; + +describe('create rules schema', () => { + test('empty objects do not validate', () => { + const payload = {}; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(message.errors.length).toBeGreaterThan(0); + expect(message.schema).toEqual({}); + }); + + test('made up values do not validate', () => { + const payload: FullCreateSchema & { madeUp: string } = { + ...getFullCreateSchemaMock(), + madeUp: 'hi', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeUp"']); + expect(message.schema).toEqual({}); + }); + + test('[rule_id] does not validate', () => { + const payload = { + rule_id: 'rule-1', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(message.errors.length).toBeGreaterThan(0); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description] does not validate', () => { + const payload = { + rule_id: 'rule-1', + description: 'some description', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(message.errors.length).toBeGreaterThan(0); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from] does not validate', () => { + const payload = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(message.errors.length).toBeGreaterThan(0); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to] does not validate', () => { + const payload = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(message.errors.length).toBeGreaterThan(0); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, name] does not validate', () => { + const payload = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(message.errors.length).toBeGreaterThan(0); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, name, severity] does not validate', () => { + const payload = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(message.errors.length).toBeGreaterThan(0); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, name, severity, type] does not validate', () => { + const payload = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "risk_score"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { + const payload = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "risk_score"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { + const payload = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + interval: '5m', + index: ['index-1'], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "risk_score"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { + const payload = { + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { + const payload = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "risk_score"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { + const payload: FullCreateSchema = { + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { + const payload: FullCreateSchema = { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { + const payload: FullCreateSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { + const payload: FullCreateSchema = { + author: [], + severity_mapping: [], + risk_score_mapping: [], + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('You can send in an empty array to threat', () => { + const payload: FullCreateSchema = { + ...getFullCreateSchemaMock(), + threat: [], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threat] does validate', () => { + const payload: FullCreateSchema = { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + threat: [ + { + framework: 'someFramework', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('allows references to be sent as valid', () => { + const payload: FullCreateSchema = { + ...getFullCreateSchemaMock(), + references: ['index-1'], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('references cannot be numbers', () => { + const payload: Omit & { references: number[] } = { + ...getFullCreateSchemaMock(), + references: [5], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "references"']); + expect(message.schema).toEqual({}); + }); + + test('indexes cannot be numbers', () => { + const payload: Omit & { index: number[] } = { + ...getFullCreateSchemaMock(), + index: [5], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "index"']); + expect(message.schema).toEqual({}); + }); + + test('saved_query type can have filters with it', () => { + const payload: SavedQueryCreateSchema = { + ...getCreateSavedQuerySchemaMock(), + filters: [], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('filters cannot be a string', () => { + const payload = { + ...getFullCreateSchemaMock(), + filters: 'some string', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "filters"', + ]); + expect(message.schema).toEqual({}); + }); + + test('language validates with kuery', () => { + const payload = { + ...getFullCreateSchemaMock(), + language: 'kuery', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('language validates with lucene', () => { + const payload = { + ...getFullCreateSchemaMock(), + language: 'lucene', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('language does not validate with something made up', () => { + const payload = { + ...getFullCreateSchemaMock(), + language: 'something-made-up', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "something-made-up" supplied to "language"', + ]); + expect(message.schema).toEqual({}); + }); + + test('max_signals cannot be negative', () => { + const payload = { + ...getFullCreateSchemaMock(), + max_signals: -1, + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "max_signals"', + ]); + expect(message.schema).toEqual({}); + }); + + test('max_signals cannot be zero', () => { + const payload = { + ...getFullCreateSchemaMock(), + max_signals: 0, + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to "max_signals"']); + expect(message.schema).toEqual({}); + }); + + test('max_signals can be 1', () => { + const payload = { + ...getFullCreateSchemaMock(), + max_signals: 1, + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('You can optionally send in an array of tags', () => { + const payload = { + ...getFullCreateSchemaMock(), + tags: ['tag_1', 'tag_2'], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('You cannot send in an array of tags that are numbers', () => { + const payload = { + ...getFullCreateSchemaMock(), + tags: [0, 1, 2], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "tags"', + 'Invalid value "1" supplied to "tags"', + 'Invalid value "2" supplied to "tags"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of threat that are missing "framework"', () => { + const payload = { + ...getFullCreateSchemaMock(), + threat: [ + { + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "threat,framework"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of threat that are missing "tactic"', () => { + const payload = { + ...getFullCreateSchemaMock(), + threat: [ + { + framework: 'fake', + technique: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "threat,tactic"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of threat that are missing "technique"', () => { + const payload = { + ...getFullCreateSchemaMock(), + threat: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + }, + ], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "threat,technique"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You can optionally send in an array of false positives', () => { + const payload = { + ...getFullCreateSchemaMock(), + false_positives: ['false_1', 'false_2'], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('You cannot send in an array of false positives that are numbers', () => { + const payload = { + ...getFullCreateSchemaMock(), + false_positives: [5, 4], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "false_positives"', + 'Invalid value "4" supplied to "false_positives"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot set the immutable to a number when trying to create a rule', () => { + const payload = { + ...getFullCreateSchemaMock(), + immutable: 5, + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "immutable"']); + expect(message.schema).toEqual({}); + }); + + test('You cannot set the risk_score to 101', () => { + const payload = { + ...getFullCreateSchemaMock(), + risk_score: 101, + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "101" supplied to "risk_score"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot set the risk_score to -1', () => { + const payload = { + ...getFullCreateSchemaMock(), + risk_score: -1, + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to "risk_score"']); + expect(message.schema).toEqual({}); + }); + + test('You can set the risk_score to 0', () => { + const payload = { + ...getFullCreateSchemaMock(), + risk_score: 0, + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('You can set the risk_score to 100', () => { + const payload = { + ...getFullCreateSchemaMock(), + risk_score: 100, + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('You can set meta to any object you want', () => { + const payload = { + ...getFullCreateSchemaMock(), + meta: { + somethingMadeUp: { somethingElse: true }, + }, + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('You cannot create meta as a string', () => { + const payload = { + ...getFullCreateSchemaMock(), + meta: 'should not work', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "should not work" supplied to "meta"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You can omit the query string when filters are present', () => { + const { query, ...noQuery } = getFullCreateSchemaMock(); + const payload = { + ...noQuery, + filters: [], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('validates with timeline_id and timeline_title', () => { + const payload = { + ...getFullCreateSchemaMock(), + timeline_id: 'timeline-id', + timeline_title: 'timeline-title', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('You cannot set the severity to a value other than low, medium, high, or critical', () => { + const payload = { + ...getFullCreateSchemaMock(), + severity: 'junk', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "junk" supplied to "severity"']); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are missing "group"', () => { + const payload = { + ...getFullCreateSchemaMock(), + actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "actions,group"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are missing "id"', () => { + const payload = { + ...getFullCreateSchemaMock(), + actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "actions,id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are missing "action_type_id"', () => { + const payload = { + ...getFullCreateSchemaMock(), + actions: [{ group: 'group', id: 'id', params: {} }], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "actions,action_type_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are missing "params"', () => { + const payload = { + ...getFullCreateSchemaMock(), + actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "actions,params"', + ]); + expect(message.schema).toEqual({}); + }); + + test('You cannot send in an array of actions that are including "actionTypeId"', () => { + const payload = { + ...getFullCreateSchemaMock(), + actions: [ + { + group: 'group', + id: 'id', + actionTypeId: 'actionTypeId', + params: {}, + }, + ], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "actions,action_type_id"', + ]); + expect(message.schema).toEqual({}); + }); + + describe('note', () => { + test('You can set note to a string', () => { + const payload = { + ...getFullCreateSchemaMock(), + note: '# documentation markdown here', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('You can set note to an empty string', () => { + const payload = { + ...getFullCreateSchemaMock(), + note: '', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('You cannot create note as an object', () => { + const payload = { + ...getFullCreateSchemaMock(), + note: { + somethingHere: 'something else', + }, + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "{"somethingHere":"something else"}" supplied to "note"', + ]); + expect(message.schema).toEqual({}); + }); + + test('empty name is not valid', () => { + const payload = { + ...getFullCreateSchemaMock(), + name: '', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "name"']); + expect(message.schema).toEqual({}); + }); + + test('empty description is not valid', () => { + const payload = { + ...getFullCreateSchemaMock(), + description: '', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "" supplied to "description"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note] does validate', () => { + const payload = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); + + test('machine_learning type does validate', () => { + const payload = { + type: 'machine_learning', + anomaly_threshold: 50, + machine_learning_job_id: 'linux_anomalous_network_activity_ecs', + false_positives: [], + references: [], + risk_score: 50, + threat: [], + name: 'ss', + description: 'ss', + severity: 'low', + tags: [], + interval: '5m', + from: 'now-360s', + to: 'now', + meta: { from: '1m' }, + actions: [], + enabled: true, + throttle: 'no_actions', + rule_id: 'rule-1', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + describe('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and exceptions_list] does validate', () => { + const payload = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: getListArrayMock(), + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { + const payload = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and invalid exceptions_list] does NOT validate', () => { + const payload = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [{ id: 'uuid_here', namespace_type: 'not a namespace type' }], + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "exceptions_list,list_id"', + 'Invalid value "undefined" supplied to "exceptions_list,type"', + 'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { + const payload = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + }; + + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); + + describe('threat_mapping', () => { + test('You can set a threat query, index, mapping, filters when creating a rule', () => { + const payload = getCreateThreatMatchSchemaMock(); + const decoded = fullCreateSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); +}); 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 a79aa838bb0ca9..a98d8c2e1cb267 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 @@ -20,7 +20,6 @@ import { event_category_override, risk_score_mapping, severity_mapping, - type, building_block_type, note, license, @@ -35,10 +34,8 @@ import { from, rule_id, immutable, - indexOrUndefined, output_index, query, - filtersOrUndefined, machine_learning_job_id, max_signals, risk_score, @@ -47,7 +44,6 @@ import { to, references, version, - savedIdOrUndefined, saved_id, threshold, anomaly_threshold, @@ -56,9 +52,7 @@ import { actions, interval, enabled, - updated_by, updated_at, - created_by, created_at, job_status, status_date, @@ -67,6 +61,8 @@ import { last_failure_at, last_failure_message, throttleOrNull, + createdByOrNull, + updatedByOrNull, } from '../common/schemas'; const createUpdateSchema = < @@ -117,303 +113,257 @@ const responseSchema = < ]); }; -const requiredRuleFields = { - description, - risk_score, - name, - severity, - type, -}; - -const optionalRuleFields = { - building_block_type, - description, - note, - license, - output_index, - timeline_id, - timeline_title, - meta, - rule_name_override, - severity, - timestamp_override, -}; - -const defaultableRuleFields = { - author, - false_positives, - from, - rule_id, - // maxSignals not used in ML rules but probably should be used - max_signals, - risk_score_mapping, - severity_mapping, - threat, - to, - references, - version, - exceptions_list: listArray, -}; - -const createUpdateRuleParams = createUpdateSchema( - requiredRuleFields, - optionalRuleFields, - defaultableRuleFields -); -const patchRuleParams = patchSchema(requiredRuleFields, optionalRuleFields, defaultableRuleFields); -const responseRuleParams = responseSchema( - requiredRuleFields, - optionalRuleFields, - defaultableRuleFields -); - -const eqlRequiredFields = { - type: t.literal('eql'), - language: t.literal('eql'), - query, -}; - -const eqlOptionalFields = { - index, - filters, - event_category_override, -}; - -const createUpdateEqlParams = createUpdateSchema(eqlRequiredFields, eqlOptionalFields, {}); -const patchEqlParams = patchSchema(eqlRequiredFields, eqlOptionalFields, {}); -const responseEqlParams = responseSchema(eqlRequiredFields, eqlOptionalFields, {}); - -const threatMatchRequiredFields = { - type: t.literal('threat_match'), - query, - threat_query, - threat_mapping, - threat_index, -}; - -const threatMatchOptionalFields = { - index, - filters, - saved_id, - threat_filters, - threat_language: t.keyof({ kuery: null, lucene: null }), -}; - -const threatMatchDefaultableFields = { - language: t.keyof({ kuery: null, lucene: null }), -}; - -const createUpdateThreatMatchParams = createUpdateSchema( - threatMatchRequiredFields, - threatMatchOptionalFields, - threatMatchDefaultableFields -); -const patchThreatMatchParams = patchSchema( - threatMatchRequiredFields, - threatMatchOptionalFields, - threatMatchDefaultableFields -); -const responseThreatMatchParams = responseSchema( - threatMatchRequiredFields, - threatMatchOptionalFields, - threatMatchDefaultableFields -); - -const queryRequiredFields = { - type: t.literal('query'), - query, +const buildAPISchemas = ( + params: RuleSpecificParams +) => { + return { + create: createUpdateSchema(params.required, params.optional, params.defaultable), + patch: patchSchema(params.required, params.optional, params.defaultable), + response: responseSchema(params.required, params.optional, params.defaultable), + }; }; -const queryOptionalFields = { - index, - filters, - saved_id, +interface RuleSpecificParams< + Required extends t.Props, + Optional extends t.Props, + Defaultable extends t.Props +> { + required: Required; + optional: Optional; + defaultable: Defaultable; +} + +const commonParams = { + required: { + name, + description, + risk_score, + severity, + }, + optional: { + building_block_type, + note, + license, + output_index, + timeline_id, + timeline_title, + meta, + rule_name_override, + timestamp_override, + }, + defaultable: { + tags, + interval, + enabled, + throttle: throttleOrNull, + actions, + author, + false_positives, + from, + rule_id, + // maxSignals not used in ML rules but probably should be used + max_signals, + risk_score_mapping, + severity_mapping, + threat, + to, + references, + version, + exceptions_list: listArray, + }, }; - -const queryDefaultableFields = { - language: t.keyof({ kuery: null, lucene: null }), +const { + create: commonCreateParams, + patch: commonPatchParams, + response: commonResponseParams, +} = buildAPISchemas(commonParams); + +const eqlRuleParams = { + required: { + type: t.literal('eql'), + language: t.literal('eql'), + query, + }, + optional: { + index, + filters, + event_category_override, + }, + defaultable: {}, }; - -const createUpdateQueryParams = createUpdateSchema( - queryRequiredFields, - queryOptionalFields, - queryDefaultableFields -); -const patchQueryParams = patchSchema( - queryRequiredFields, - queryOptionalFields, - queryDefaultableFields -); -const responseQueryParams = responseSchema( - queryRequiredFields, - queryOptionalFields, - queryDefaultableFields -); - -const savedQueryRequiredFields = { - type: t.literal('saved_query'), - saved_id, +const { + create: eqlCreateParams, + patch: eqlPatchParams, + response: eqlResponseParams, +} = buildAPISchemas(eqlRuleParams); + +const threatMatchRuleParams = { + required: { + type: t.literal('threat_match'), + query, + threat_query, + threat_mapping, + threat_index, + }, + optional: { + index, + filters, + saved_id, + threat_filters, + threat_language: t.keyof({ kuery: null, lucene: null }), + }, + defaultable: { + language: t.keyof({ kuery: null, lucene: null }), + }, }; - -const savedQueryOptionalFields = { - // Having language, query, and filters possibly defined adds more code confusion and probably user confusion - // if the saved object gets deleted for some reason - index, - query, - filters, +const { + create: threatMatchCreateParams, + patch: threatMatchPatchParams, + response: threatMatchResponseParams, +} = buildAPISchemas(threatMatchRuleParams); + +const queryRuleParams = { + required: { + type: t.literal('query'), + }, + optional: { + index, + filters, + saved_id, + }, + defaultable: { + query, + language: t.keyof({ kuery: null, lucene: null }), + }, }; - -const savedQueryDefaultableFields = { - language: t.keyof({ kuery: null, lucene: null }), +const { + create: queryCreateParams, + patch: queryPatchParams, + response: queryResponseParams, +} = buildAPISchemas(queryRuleParams); + +const savedQueryRuleParams = { + required: { + type: t.literal('saved_query'), + saved_id, + }, + optional: { + // Having language, query, and filters possibly defined adds more code confusion and probably user confusion + // if the saved object gets deleted for some reason + index, + query, + filters, + }, + defaultable: { + language: t.keyof({ kuery: null, lucene: null }), + }, }; - -const createUpdateSavedQueryParams = createUpdateSchema( - savedQueryRequiredFields, - savedQueryOptionalFields, - savedQueryDefaultableFields -); -const patchSavedQueryParams = patchSchema( - savedQueryRequiredFields, - savedQueryOptionalFields, - savedQueryDefaultableFields -); -const responseSavedQueryParams = responseSchema( - savedQueryRequiredFields, - savedQueryOptionalFields, - savedQueryDefaultableFields -); - -const thresholdRequiredFields = { - type: t.literal('threshold'), - query, - threshold, +const { + create: savedQueryCreateParams, + patch: savedQueryPatchParams, + response: savedQueryResponseParams, +} = buildAPISchemas(savedQueryRuleParams); + +const thresholdRuleParams = { + required: { + type: t.literal('threshold'), + query, + threshold, + }, + optional: { + index, + filters, + saved_id, + }, + defaultable: { + language: t.keyof({ kuery: null, lucene: null }), + }, }; - -const thresholdOptionalFields = { - index: indexOrUndefined, - filters: filtersOrUndefined, - saved_id: savedIdOrUndefined, +const { + create: thresholdCreateParams, + patch: thresholdPatchParams, + response: thresholdResponseParams, +} = buildAPISchemas(thresholdRuleParams); + +const machineLearningRuleParams = { + required: { + type: t.literal('machine_learning'), + anomaly_threshold, + machine_learning_job_id, + }, + optional: {}, + defaultable: {}, }; +const { + create: machineLearningCreateParams, + patch: machineLearningPatchParams, + response: machineLearningResponseParams, +} = buildAPISchemas(machineLearningRuleParams); + +const createTypeSpecific = t.union([ + eqlCreateParams, + threatMatchCreateParams, + queryCreateParams, + savedQueryCreateParams, + thresholdCreateParams, + machineLearningCreateParams, +]); +export type CreateTypeSpecific = t.TypeOf; -const thresholdDefaultableFields = { - language: t.keyof({ kuery: null, lucene: null }), -}; +export const eqlCreateSchema = t.intersection([eqlCreateParams, commonCreateParams]); +export type EqlCreateSchema = t.TypeOf; -const createUpdateThresholdParams = createUpdateSchema( - thresholdRequiredFields, - thresholdOptionalFields, - thresholdDefaultableFields -); -const patchThresholdParams = patchSchema( - thresholdRequiredFields, - thresholdOptionalFields, - thresholdDefaultableFields -); -const responseThresholdParams = responseSchema( - thresholdRequiredFields, - thresholdOptionalFields, - thresholdDefaultableFields -); +export const threatMatchCreateSchema = t.intersection([ + threatMatchCreateParams, + commonCreateParams, +]); +export type ThreatMatchCreateSchema = t.TypeOf; -const machineLearningRequiredFields = { - type: t.literal('machine_learning'), - anomaly_threshold, - machine_learning_job_id, -}; +export const queryCreateSchema = t.intersection([queryCreateParams, commonCreateParams]); +export type QueryCreateSchema = t.TypeOf; -const machineLearningOptionalFields = {}; +export const savedQueryCreateSchema = t.intersection([savedQueryCreateParams, commonCreateParams]); +export type SavedQueryCreateSchema = t.TypeOf; -const createUpdateMachineLearningParams = createUpdateSchema( - machineLearningRequiredFields, - machineLearningOptionalFields, - {} -); -const patchMachineLearningParams = patchSchema( - machineLearningRequiredFields, - machineLearningOptionalFields, - {} -); -const responseMachineLearningParams = responseSchema( - machineLearningRequiredFields, - machineLearningOptionalFields, - {} -); +export const thresholdCreateSchema = t.intersection([thresholdCreateParams, commonCreateParams]); +export type ThresholdCreateSchema = t.TypeOf; -const createUpdateTypeSpecific = t.union([ - createUpdateEqlParams, - createUpdateThreatMatchParams, - createUpdateQueryParams, - createUpdateSavedQueryParams, - createUpdateThresholdParams, - createUpdateMachineLearningParams, +export const machineLearningCreateSchema = t.intersection([ + machineLearningCreateParams, + commonCreateParams, ]); -export type CreateUpdateTypeSpecific = t.TypeOf; +export type MachineLearningCreateSchema = t.TypeOf; + +export const fullCreateSchema = t.intersection([commonCreateParams, createTypeSpecific]); +export type FullCreateSchema = t.TypeOf; const patchTypeSpecific = t.union([ - patchEqlParams, - patchThreatMatchParams, - patchQueryParams, - patchSavedQueryParams, - patchThresholdParams, - patchMachineLearningParams, + eqlPatchParams, + threatMatchPatchParams, + queryPatchParams, + savedQueryPatchParams, + thresholdPatchParams, + machineLearningPatchParams, ]); const responseTypeSpecific = t.union([ - responseEqlParams, - responseThreatMatchParams, - responseQueryParams, - responseSavedQueryParams, - responseThresholdParams, - responseMachineLearningParams, + eqlResponseParams, + threatMatchResponseParams, + queryResponseParams, + savedQueryResponseParams, + thresholdResponseParams, + machineLearningResponseParams, ]); - -const coreRequiredRuleFields = { - name, -}; - -const coreOptionalRuleFields = {}; - -const coreDefaultableRuleFields = { - tags, - interval, - enabled, - throttle: throttleOrNull, - actions, -}; - -const createUpdateCoreParams = createUpdateSchema( - coreRequiredRuleFields, - coreOptionalRuleFields, - coreDefaultableRuleFields -); -const patchCoreParams = patchSchema( - coreRequiredRuleFields, - coreOptionalRuleFields, - coreDefaultableRuleFields -); -const responseCoreParams = responseSchema( - coreRequiredRuleFields, - coreOptionalRuleFields, - coreDefaultableRuleFields -); - -export const fullCreateSchema = t.intersection([ - createUpdateCoreParams, - createUpdateRuleParams, - createUpdateTypeSpecific, -]); -export type FullCreateSchema = t.TypeOf; +export type ResponseTypeSpecific = t.TypeOf; export const fullUpdateSchema = t.intersection([ - createUpdateCoreParams, - createUpdateRuleParams, - createUpdateTypeSpecific, + commonCreateParams, + createTypeSpecific, t.exact(t.partial({ id })), ]); export const fullPatchSchema = t.intersection([ - patchCoreParams, - patchRuleParams, + commonPatchParams, patchTypeSpecific, t.exact(t.partial({ id })), ]); @@ -422,9 +372,9 @@ const responseRequiredFields = { id, immutable, updated_at, - updated_by, + updated_by: updatedByOrNull, created_at, - created_by, + created_by: createdByOrNull, }; const responseOptionalFields = { status: job_status, @@ -436,8 +386,7 @@ const responseOptionalFields = { }; export const fullResponseSchema = t.intersection([ - responseCoreParams, - responseRuleParams, + commonResponseParams, responseTypeSpecific, t.exact(t.type(responseRequiredFields)), t.exact(t.partial(responseOptionalFields)), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index 4f398203bc1e38..cd375735062326 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -36,7 +36,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void { path: DETECTION_ENGINE_RULES_URL, validate: { - body: buildRouteValidation(fullCreateSchema), + body: buildRouteValidation(fullCreateSchema), }, options: { tags: ['access:securitySolution'], @@ -91,17 +91,17 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void meta: request.body.meta, maxSignals: request.body.max_signals ?? DEFAULT_MAX_SIGNALS, riskScore: request.body.risk_score, - riskScoreMapping: request.body.risk_score_mapping, + riskScoreMapping: request.body.risk_score_mapping ?? [], ruleNameOverride: request.body.rule_name_override, severity: request.body.severity, - severityMapping: request.body.severity_mapping, + severityMapping: request.body.severity_mapping ?? [], threat: request.body.threat ?? [], timestampOverride: request.body.timestamp_override, to: request.body.to ?? 'now', references: request.body.references ?? [], note: request.body.note, version: request.body.version ?? 1, - exceptionsList: request.body.exceptions_list, + exceptionsList: request.body.exceptions_list ?? [], ...typeSpecificParams, }, schedule: { interval: request.body.interval ?? '5m' }, 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 512fc2bb31d898..4c5646eacb06b8 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 @@ -4,13 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TypeSpecificRuleParams } from './rule_schemas'; +import { InternalRuleResponse, TypeSpecificRuleParams } from './rule_schemas'; import { assertUnreachable } from '../../../../common/utility_types'; -import { CreateUpdateTypeSpecific } from '../../../../common/detection_engine/schemas/request/rule_schemas'; +import { + CreateTypeSpecific, + FullResponseSchema, + ResponseTypeSpecific, +} from '../../../../common/detection_engine/schemas/request/rule_schemas'; +import { RuleActions } from '../rule_actions/types'; -export const typeSpecificSnakeToCamel = ( - params: CreateUpdateTypeSpecific -): TypeSpecificRuleParams => { +// These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema +// to the response API schema. This provides static type-check assurances that the internal schema is in sync with the API schema for +// required and defaultable fields. However, it is still possible to add an optional field to the API schema +// without causing a type-check error here. + +// Converts params from the snake case API format to the internal camel case format AND applies default values where needed. +// Notice that params.language is possibly undefined for most rule types in the API but we default it to kuery to match +// the legacy API behavior +export const typeSpecificSnakeToCamel = (params: CreateTypeSpecific): TypeSpecificRuleParams => { switch (params.type) { case 'eql': { return { @@ -42,7 +53,7 @@ export const typeSpecificSnakeToCamel = ( type: params.type, language: params.language ?? 'kuery', index: params.index, - query: params.query, + query: params.query ?? '', filters: params.filters, savedId: params.saved_id, }; @@ -50,7 +61,7 @@ export const typeSpecificSnakeToCamel = ( case 'saved_query': { return { type: params.type, - language: params.language, + language: params.language ?? 'kuery', index: params.index, query: params.query, filters: params.filters, @@ -80,3 +91,120 @@ export const typeSpecificSnakeToCamel = ( } } }; + +// Converts the internal rule data structure to the response API schema +export const typeSpecificCamelToSnake = (params: TypeSpecificRuleParams): ResponseTypeSpecific => { + switch (params.type) { + case 'eql': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + event_category_override: params.eventCategoryOverride, + }; + } + case 'threat_match': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + threat_filters: params.threatFilters, + threat_query: params.threatQuery, + threat_mapping: params.threatMapping, + threat_language: params.threatLanguage, + threat_index: params.threatIndex, + }; + } + case 'query': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + }; + } + case 'saved_query': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + }; + } + case 'threshold': { + return { + type: params.type, + language: params.language, + index: params.index, + query: params.query, + filters: params.filters, + saved_id: params.savedId, + threshold: params.threshold, + }; + } + case 'machine_learning': { + return { + type: params.type, + anomaly_threshold: params.anomalyThreshold, + machine_learning_job_id: params.machineLearningJobId, + }; + } + default: { + return assertUnreachable(params); + } + } +}; + +export const internalRuleToAPIResponse = ( + rule: InternalRuleResponse, + ruleActions: RuleActions +): FullResponseSchema => { + return { + id: rule.id, + immutable: rule.params.immutable, + updated_at: rule.updatedAt, + updated_by: rule.updatedBy, + created_at: rule.createdAt, + created_by: rule.createdBy, + name: rule.name, + tags: rule.tags, + interval: rule.schedule.interval, + enabled: rule.enabled, + throttle: ruleActions.ruleThrottle, + actions: ruleActions.actions, + description: rule.params.description, + risk_score: rule.params.riskScore, + severity: rule.params.severity, + building_block_type: rule.params.buildingBlockType, + note: rule.params.note, + license: rule.params.license, + output_index: rule.params.outputIndex, + timeline_id: rule.params.timelineId, + timeline_title: rule.params.timelineTitle, + meta: rule.params.meta, + rule_name_override: rule.params.ruleNameOverride, + timestamp_override: rule.params.timestampOverride, + author: rule.params.author, + false_positives: rule.params.falsePositives, + from: rule.params.from, + rule_id: rule.params.ruleId, + max_signals: rule.params.maxSignals, + risk_score_mapping: rule.params.riskScoreMapping, + severity_mapping: rule.params.severityMapping, + threat: rule.params.threat, + to: rule.params.to, + references: rule.params.references, + version: rule.params.version, + exceptions_list: rule.params.exceptionsList, + ...typeSpecificCamelToSnake(rule.params), + }; +}; 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 85663b0a74c934..75061d80cbf448 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 @@ -6,7 +6,7 @@ import * as t from 'io-ts'; -import { listArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists'; +import { listArray } from '../../../../common/detection_engine/schemas/types/lists'; import { threat_mapping, threat_index, @@ -35,10 +35,10 @@ import { machine_learning_job_id, max_signals, risk_score, - riskScoreMappingOrUndefined, + risk_score_mapping, ruleNameOverrideOrUndefined, severity, - severityMappingOrUndefined, + severity_mapping, tags, timestampOverrideOrUndefined, threat, @@ -52,6 +52,10 @@ import { anomaly_threshold, actionsCamel, throttleOrNull, + createdByOrNull, + updatedByOrNull, + created_at, + updated_at, } from '../../../../common/detection_engine/schemas/common/schemas'; import { SIGNALS_ID, SERVER_APP_ID } from '../../../../common/constants'; @@ -74,16 +78,16 @@ export const baseRuleParams = t.exact( // maxSignals not used in ML rules but probably should be used maxSignals: max_signals, riskScore: risk_score, - riskScoreMapping: riskScoreMappingOrUndefined, + riskScoreMapping: risk_score_mapping, ruleNameOverride: ruleNameOverrideOrUndefined, severity, - severityMapping: severityMappingOrUndefined, + severityMapping: severity_mapping, timestampOverride: timestampOverrideOrUndefined, threat, to, references, version, - exceptionsList: listArrayOrUndefined, + exceptionsList: listArray, }) ); export type BaseRuleParams = t.TypeOf; @@ -126,7 +130,7 @@ const savedQuerySpecificRuleParams = t.type({ type: t.literal('saved_query'), // Having language, query, and filters possibly defined adds more code confusion and probably user confusion // if the saved object gets deleted for some reason - language: t.union([nonEqlLanguages, t.undefined]), + language: nonEqlLanguages, index: indexOrUndefined, query: queryOrUndefined, filters: filtersOrUndefined, @@ -176,3 +180,15 @@ export const internalRuleCreate = t.type({ throttle: throttleOrNull, }); export type InternalRuleCreate = t.TypeOf; + +export const internalRuleResponse = t.intersection([ + internalRuleCreate, + t.type({ + id: t.string, + createdBy: createdByOrNull, + updatedBy: updatedByOrNull, + createdAt: created_at, + updatedAt: updated_at, + }), +]); +export type InternalRuleResponse = t.TypeOf; From 328676e7f6989dd4d5ded5ad8e40a05b8ddd4c1d Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Thu, 5 Nov 2020 19:14:04 -0500 Subject: [PATCH 05/19] Naming updates and linting fixes --- .../schemas/request/rule_schemas.test.ts | 1 - .../detection_engine/schemas/request/rule_schemas.ts | 9 +++++---- .../security_and_spaces/tests/create_threat_matching.ts | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts index 5e3092dd01b78d..7d079cacaaac22 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts @@ -14,7 +14,6 @@ import { getCreateThreatMatchSchemaMock, getFullCreateSchemaMock, } from './rule_schemas.mock'; -import { DEFAULT_MAX_SIGNALS } from '../../../constants'; import { getListArrayMock } from '../types/lists.mock'; describe('create rules schema', () => { 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 a98d8c2e1cb267..8d53caab1e62c4 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 @@ -65,7 +65,7 @@ import { updatedByOrNull, } from '../common/schemas'; -const createUpdateSchema = < +const createSchema = < Required extends t.Props, Optional extends t.Props, Defaultable extends t.Props @@ -114,16 +114,16 @@ const responseSchema = < }; const buildAPISchemas = ( - params: RuleSpecificParams + params: APIParams ) => { return { - create: createUpdateSchema(params.required, params.optional, params.defaultable), + create: createSchema(params.required, params.optional, params.defaultable), patch: patchSchema(params.required, params.optional, params.defaultable), response: responseSchema(params.required, params.optional, params.defaultable), }; }; -interface RuleSpecificParams< +interface APIParams< Required extends t.Props, Optional extends t.Props, Defaultable extends t.Props @@ -310,6 +310,7 @@ const createTypeSpecific = t.union([ ]); export type CreateTypeSpecific = t.TypeOf; +// Convenience types for building specific types of rules export const eqlCreateSchema = t.intersection([eqlCreateParams, commonCreateParams]); export type EqlCreateSchema = t.TypeOf; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index ca00ce06ffdf2c..5288f1bc81826a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -23,7 +23,7 @@ import { waitForSignalsToBePresent, } from '../../utils'; -import { getFullCreateThreatMatchSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock'; +import { getCreateThreatMatchSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock'; import { getThreatMatchingSchemaPartialMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks'; // eslint-disable-next-line import/no-default-export @@ -41,7 +41,7 @@ export default ({ getService }: FtrProviderContext) => { const { body } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getFullCreateThreatMatchSchemaMock()) + .send(getCreateThreatMatchSchemaMock()) .expect(400); expect(body).to.eql({ @@ -63,13 +63,13 @@ export default ({ getService }: FtrProviderContext) => { }); it('should create a single rule with a rule_id', async () => { - const ruleResponse = await createRule(supertest, getFullCreateThreatMatchSchemaMock()); + const ruleResponse = await createRule(supertest, getCreateThreatMatchSchemaMock()); const bodyToCompare = removeServerGeneratedProperties(ruleResponse); expect(bodyToCompare).to.eql(getThreatMatchingSchemaPartialMock()); }); it('should create a single rule with a rule_id and validate it ran successfully', async () => { - const ruleResponse = await createRule(supertest, getFullCreateThreatMatchSchemaMock()); + const ruleResponse = await createRule(supertest, getCreateThreatMatchSchemaMock()); await waitForRuleSuccess(supertest, ruleResponse.id); const { body: statusBody } = await supertest From c41aecb0ebb01b7c5b24ddd653b611da9e8b126e Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Sun, 8 Nov 2020 10:11:20 -0500 Subject: [PATCH 06/19] Replace create_rules_bulk_schema and refactor route --- .../request/create_rules_bulk_schema.ts | 6 +- .../rules/create/helpers.test.ts | 8 +- .../routes/__mocks__/request_responses.ts | 18 +- .../rules/create_rules_bulk_route.test.ts | 16 +- .../routes/rules/create_rules_bulk_route.ts | 219 +++++++----------- .../routes/rules/create_rules_route.test.ts | 10 +- .../detection_engine/routes/rules/utils.ts | 7 +- 7 files changed, 114 insertions(+), 170 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts index c6233cc63fa9fb..1b800e7de2df98 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts @@ -6,9 +6,7 @@ import * as t from 'io-ts'; -import { createRulesSchema, CreateRulesSchemaDecoded } from './create_rules_schema'; +import { fullCreateSchema } from './rule_schemas'; -export const createRulesBulkSchema = t.array(createRulesSchema); +export const createRulesBulkSchema = t.array(fullCreateSchema); export type CreateRulesBulkSchema = t.TypeOf; - -export type CreateRulesBulkSchemaDecoded = CreateRulesSchemaDecoded[]; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts index 11222a0a95a80a..f79daa2b334519 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts @@ -5,7 +5,7 @@ */ import { List } from '../../../../../../common/detection_engine/schemas/types'; -import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request/create_rules_schema'; +import { FullCreateSchema } from '../../../../../../common/detection_engine/schemas/request/rule_schemas'; import { Rule } from '../../../../containers/detection_engine/rules'; import { getListMock, @@ -762,7 +762,7 @@ describe('helpers', () => { saved_id: '', }, }; - const result: CreateRulesSchema = formatRule( + const result: FullCreateSchema = formatRule( mockDefineStepRuleWithoutSavedId, mockAbout, mockSchedule, @@ -773,14 +773,14 @@ describe('helpers', () => { }); test('returns rule without id if ruleId does not exist', () => { - const result: CreateRulesSchema = formatRule( + const result: FullCreateSchema = formatRule( mockDefine, mockAbout, mockSchedule, mockActions ); - expect(result).not.toHaveProperty('id'); + expect(result).not.toHaveProperty('id'); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 94b820344b37ca..c953bf9de19cdc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -26,9 +26,9 @@ import { requestMock } from './request'; import { RuleNotificationAlertType } from '../../notifications/types'; import { QuerySignalsSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema'; import { SetSignalsStatusSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { EqlSearchResponse } from '../../../../../common/detection_engine/types'; +import { getFullCreateSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ signal_ids: ['somefakeid1', 'somefakeid2'], @@ -56,14 +56,14 @@ export const getUpdateRequest = () => requestMock.create({ method: 'put', path: DETECTION_ENGINE_RULES_URL, - body: getCreateRulesSchemaMock(), + body: getFullCreateSchemaMock(), }); export const getPatchRequest = () => requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_URL, - body: getCreateRulesSchemaMock(), + body: getFullCreateSchemaMock(), }); export const getReadRequest = () => @@ -83,21 +83,21 @@ export const getReadBulkRequest = () => requestMock.create({ method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - body: [getCreateRulesSchemaMock()], + body: [getFullCreateSchemaMock()], }); export const getUpdateBulkRequest = () => requestMock.create({ method: 'put', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, - body: [getCreateRulesSchemaMock()], + body: [getFullCreateSchemaMock()], }); export const getPatchBulkRequest = () => requestMock.create({ method: 'patch', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, - body: [getCreateRulesSchemaMock()], + body: [getFullCreateSchemaMock()], }); export const getDeleteBulkRequest = () => @@ -233,12 +233,12 @@ export const getCreateRequest = () => requestMock.create({ method: 'post', path: DETECTION_ENGINE_RULES_URL, - body: getCreateRulesSchemaMock(), + body: getFullCreateSchemaMock(), }); // TODO: Replace this with the mocks version from the mocks file export const typicalMlRulePayload = () => { - const { query, language, index, ...mlParams } = getCreateRulesSchemaMock(); + const { query, language, index, ...mlParams } = getFullCreateSchemaMock(); return { ...mlParams, @@ -266,7 +266,7 @@ export const createBulkMlRuleRequest = () => { // TODO: Replace this with a mocks file version export const createRuleWithActionsRequest = () => { - const payload = getCreateRulesSchemaMock(); + const payload = getFullCreateSchemaMock(); return requestMock.create({ method: 'post', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 06fcba36642caa..fe7f0164400b2f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -18,7 +18,7 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesBulkRoute } from './create_rules_bulk_route'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getFullCreateSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -133,7 +133,7 @@ describe('create_rules_bulk', () => { const request = requestMock.create({ method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - body: [getCreateRulesSchemaMock(), getCreateRulesSchemaMock()], + body: [getFullCreateSchemaMock(), getFullCreateSchemaMock()], }); const response = await server.inject(request, context); @@ -154,7 +154,7 @@ describe('create_rules_bulk', () => { const request = requestMock.create({ method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - body: [{ ...getCreateRulesSchemaMock(), type: 'query' }], + body: [{ ...getFullCreateSchemaMock(), type: 'query' }], }); const result = server.validate(request); @@ -165,7 +165,7 @@ describe('create_rules_bulk', () => { const request = requestMock.create({ method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - body: [{ from: 'now-7m', interval: '5m', ...getCreateRulesSchemaMock() }], + body: [{ from: 'now-7m', interval: '5m', ...getFullCreateSchemaMock() }], }); const result = server.validate(request); @@ -176,13 +176,11 @@ describe('create_rules_bulk', () => { const request = requestMock.create({ method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - body: [{ ...getCreateRulesSchemaMock(), type: 'unexpected_type' }], + body: [{ ...getFullCreateSchemaMock(), type: 'unexpected_type' }], }); const result = server.validate(request); - expect(result.badRequest).toHaveBeenCalledWith( - 'Invalid value "unexpected_type" supplied to "type"' - ); + expect(result.badRequest).toHaveBeenCalled(); }); test('disallows invalid "from" param on rule', async () => { @@ -193,7 +191,7 @@ describe('create_rules_bulk', () => { { from: 'now-3755555555555555.67s', interval: '5m', - ...getCreateRulesSchemaMock(), + ...getFullCreateSchemaMock(), }, ], }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 8c7a19869ce188..8687f56936efa7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -4,20 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable complexity */ + +import uuid from 'uuid'; +import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions'; import { validate } from '../../../../../common/validate'; import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents'; -import { RuleAlertAction } from '../../../../../common/detection_engine/types'; -import { - CreateRulesBulkSchemaDecoded, - createRulesBulkSchema, -} from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; +import { createRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; import { IRouter } from '../../../../../../../../src/core/server'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { + DEFAULT_MAX_SIGNALS, + DETECTION_ENGINE_RULES_URL, + SERVER_APP_ID, + SIGNALS_ID, +} from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; -import { createRules } from '../../rules/create_rules'; import { readRules } from '../../rules/read_rules'; import { getDuplicates } from './utils'; import { transformValidateBulkError } from './validate'; @@ -26,17 +30,16 @@ import { buildRouteValidation } from '../../../../utils/build_validation/route_v import { transformBulkError, createBulkErrorObject, buildSiemResponse } from '../utils'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; -import { PartialFilter } from '../../types'; -import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { typeSpecificSnakeToCamel } from '../../schemas/rule_converters'; +import { InternalRuleCreate } from '../../schemas/rule_schemas'; +import { addTags } from '../../rules/add_tags'; export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => { router.post( { path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, validate: { - body: buildRouteValidation( - createRulesBulkSchema - ), + body: buildRouteValidation(createRulesBulkSchema), }, options: { tags: ['access:securitySolution'], @@ -67,154 +70,102 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => ruleDefinitions .filter((rule) => rule.rule_id == null || !dupes.includes(rule.rule_id)) .map(async (payloadRule) => { - const { - actions: actionsRest, - anomaly_threshold: anomalyThreshold, - author, - building_block_type: buildingBlockType, - description, - enabled, - event_category_override: eventCategoryOverride, - false_positives: falsePositives, - from, - query: queryOrUndefined, - language: languageOrUndefined, - license, - machine_learning_job_id: machineLearningJobId, - output_index: outputIndex, - saved_id: savedId, - meta, - filters: filtersRest, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - risk_score_mapping: riskScoreMapping, - rule_name_override: ruleNameOverride, - name, - severity, - severity_mapping: severityMapping, - tags, - threat, - threat_filters: threatFilters, - threat_index: threatIndex, - threat_mapping: threatMapping, - threat_query: threatQuery, - threat_language: threatLanguage, - threshold, - throttle, - timestamp_override: timestampOverride, - to, - type, - references, - note, - timeline_id: timelineId, - timeline_title: timelineTitle, - version, - exceptions_list: exceptionsList, - } = payloadRule; + if (payloadRule.rule_id != null) { + const rule = await readRules({ + alertsClient, + ruleId: payloadRule.rule_id, + id: undefined, + }); + if (rule != null) { + return createBulkErrorObject({ + ruleId: payloadRule.rule_id, + statusCode: 409, + message: `rule_id: "${payloadRule.rule_id}" already exists`, + }); + } + } + const typeSpecificParams = typeSpecificSnakeToCamel(payloadRule); + const newRuleId = payloadRule.rule_id ?? uuid.v4(); + const throttle = payloadRule.throttle ?? null; + const internalRule: InternalRuleCreate = { + name: payloadRule.name, + tags: addTags(payloadRule.tags ?? [], newRuleId, false), + alertTypeId: SIGNALS_ID, + consumer: SERVER_APP_ID, + params: { + author: payloadRule.author ?? [], + buildingBlockType: payloadRule.building_block_type, + description: payloadRule.description, + ruleId: newRuleId, + falsePositives: payloadRule.false_positives ?? [], + from: payloadRule.from ?? 'now-6m', + immutable: false, + license: payloadRule.license, + outputIndex: payloadRule.output_index ?? siemClient.getSignalsIndex(), + timelineId: payloadRule.timeline_id, + timelineTitle: payloadRule.timeline_title, + meta: payloadRule.meta, + maxSignals: payloadRule.max_signals ?? DEFAULT_MAX_SIGNALS, + riskScore: payloadRule.risk_score, + riskScoreMapping: payloadRule.risk_score_mapping ?? [], + ruleNameOverride: payloadRule.rule_name_override, + severity: payloadRule.severity, + severityMapping: payloadRule.severity_mapping ?? [], + threat: payloadRule.threat ?? [], + timestampOverride: payloadRule.timestamp_override, + to: payloadRule.to ?? 'now', + references: payloadRule.references ?? [], + note: payloadRule.note, + version: payloadRule.version ?? 1, + exceptionsList: payloadRule.exceptions_list ?? [], + ...typeSpecificParams, + }, + schedule: { interval: payloadRule.interval ?? '5m' }, + enabled: payloadRule.enabled ?? true, + actions: + throttle === 'rule' + ? (payloadRule.actions ?? []).map(transformRuleToAlertAction) + : [], + throttle: null, + }; try { const validationErrors = createRuleValidateTypeDependents(payloadRule); if (validationErrors.length) { return createBulkErrorObject({ - ruleId, + ruleId: internalRule.params.ruleId, statusCode: 400, message: validationErrors.join(), }); } - 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 actions: RuleAlertAction[] = actionsRest as RuleAlertAction[]; - const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; - throwHttpError(await mlAuthz.validateRuleType(type)); - - const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); + throwHttpError(await mlAuthz.validateRuleType(internalRule.params.type)); + const finalIndex = internalRule.params.outputIndex; const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); if (!indexExists) { return createBulkErrorObject({ - ruleId, + ruleId: internalRule.params.ruleId, statusCode: 400, message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, }); } - if (ruleId != null) { - const rule = await readRules({ alertsClient, ruleId, id: undefined }); - if (rule != null) { - return createBulkErrorObject({ - ruleId, - statusCode: 409, - message: `rule_id: "${ruleId}" already exists`, - }); - } - } - const createdRule = await createRules({ - alertsClient, - anomalyThreshold, - author, - buildingBlockType, - description, - enabled, - eventCategoryOverride, - falsePositives, - from, - immutable: false, - query, - language, - license, - machineLearningJobId, - outputIndex: finalIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - ruleId, - index, - interval, - maxSignals, - name, - riskScore, - riskScoreMapping, - ruleNameOverride, - severity, - severityMapping, - tags, - to, - type, - threat, - threatFilters, - threatMapping, - threatQuery, - threatIndex, - threatLanguage, - threshold, - timestampOverride, - references, - note, - version, - exceptionsList, - actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is set to rule, otherwise we are a notification and should not enable it, + + const createdRule = await alertsClient.create({ + data: internalRule, }); const ruleActions = await updateRulesNotifications({ ruleAlertId: createdRule.id, alertsClient, savedObjectsClient, - enabled, - actions, + enabled: createdRule.enabled, + actions: payloadRule.actions, throttle, - name, + name: createdRule.name, }); - return transformValidateBulkError(ruleId, createdRule, ruleActions); + return transformValidateBulkError(newRuleId, createdRule, ruleActions); } catch (err) { - return transformBulkError(ruleId, err); + return transformBulkError(newRuleId, err); } }) ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 9390f1ab1973e3..9408ad3a1499de 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -20,7 +20,7 @@ import { buildMlAuthz } from '../../../machine_learning/authz'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesRoute } from './create_rules_route'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getFullCreateSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('../../rules/update_rules_notifications'); jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -140,7 +140,7 @@ describe('create_rules', () => { method: 'post', path: DETECTION_ENGINE_RULES_URL, body: { - ...getCreateRulesSchemaMock(), + ...getFullCreateSchemaMock(), type: 'query', }, }); @@ -154,7 +154,7 @@ describe('create_rules', () => { method: 'post', path: DETECTION_ENGINE_RULES_URL, body: { - ...getCreateRulesSchemaMock(), + ...getFullCreateSchemaMock(), type: 'unexpected_type', }, }); @@ -167,7 +167,7 @@ describe('create_rules', () => { const request = requestMock.create({ method: 'post', path: DETECTION_ENGINE_RULES_URL, - body: { from: 'now-7m', interval: '5m', ...getCreateRulesSchemaMock() }, + body: { from: 'now-7m', interval: '5m', ...getFullCreateSchemaMock() }, }); const result = server.validate(request); @@ -181,7 +181,7 @@ describe('create_rules', () => { body: { from: 'now-3755555555555555.67s', interval: '5m', - ...getCreateRulesSchemaMock(), + ...getFullCreateSchemaMock(), }, }); const result = server.validate(request); 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 fb4ba855f65369..aa692c001496ac 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 @@ -10,7 +10,7 @@ import uuid from 'uuid'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; -import { CreateRulesBulkSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; +import { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import { PartialAlert, FindResult } from '../../../../../../alerts/server'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { @@ -254,10 +254,7 @@ export const transformOrImportError = ( } }; -export const getDuplicates = ( - ruleDefinitions: CreateRulesBulkSchemaDecoded, - by: 'rule_id' -): string[] => { +export const getDuplicates = (ruleDefinitions: CreateRulesBulkSchema, by: 'rule_id'): string[] => { const mappedDuplicates = countBy( by, ruleDefinitions.filter((r) => r[by] != null) From 428685dc49f557ef8d0c90ecad11832dc0c07c91 Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Mon, 9 Nov 2020 18:26:08 -0500 Subject: [PATCH 07/19] Convert update_rules_route to new schema --- .../schemas/request/rule_schemas.ts | 5 + .../request/update_rules_bulk_schema.ts | 7 +- .../rules/update_rules_bulk_route.test.ts | 4 +- .../routes/rules/update_rules_bulk_route.ts | 135 +---------- .../routes/rules/update_rules_route.test.ts | 4 +- .../routes/rules/update_rules_route.ts | 132 +---------- .../lib/detection_engine/rules/types.ts | 51 +--- .../detection_engine/rules/update_rules.ts | 218 ++++++------------ .../schemas/rule_converters.ts | 4 + .../detection_engine/schemas/rule_schemas.ts | 4 + 10 files changed, 108 insertions(+), 456 deletions(-) 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 8d53caab1e62c4..1527390abf059d 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 @@ -12,6 +12,8 @@ import { threat_query, threat_mapping, threat_index, + concurrent_searches, + items_per_search, } from '../types/threat_mapping'; import { id, @@ -211,6 +213,8 @@ const threatMatchRuleParams = { saved_id, threat_filters, threat_language: t.keyof({ kuery: null, lucene: null }), + concurrent_searches, + items_per_search, }, defaultable: { language: t.keyof({ kuery: null, lucene: null }), @@ -362,6 +366,7 @@ export const fullUpdateSchema = t.intersection([ createTypeSpecific, t.exact(t.partial({ id })), ]); +export type FullUpdateSchema = t.TypeOf; export const fullPatchSchema = t.intersection([ commonPatchParams, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.ts index 429103c7df13e2..1596f995d8b8b7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.ts @@ -5,10 +5,7 @@ */ import * as t from 'io-ts'; +import { fullUpdateSchema } from './rule_schemas'; -import { updateRulesSchema, UpdateRulesSchemaDecoded } from './update_rules_schema'; - -export const updateRulesBulkSchema = t.array(updateRulesSchema); +export const updateRulesBulkSchema = t.array(fullUpdateSchema); export type UpdateRulesBulkSchema = t.TypeOf; - -export type UpdateRulesBulkSchemaDecoded = UpdateRulesSchemaDecoded[]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index ec5a2be255a2cf..48071e72ffe6d7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -150,9 +150,7 @@ describe('update_rules_bulk', () => { }); const result = server.validate(request); - expect(result.badRequest).toHaveBeenCalledWith( - 'Invalid value "unknown_type" supplied to "type"' - ); + expect(result.badRequest).toHaveBeenCalled(); }); test('allows rule type of query and custom from and interval', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index f4a31c2bb456dd..df2fecf3dae7ab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -6,14 +6,9 @@ import { validate } from '../../../../../common/validate'; import { updateRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/update_rules_type_dependents'; -import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import { - updateRulesBulkSchema, - UpdateRulesBulkSchemaDecoded, -} from '../../../../../common/detection_engine/schemas/request/update_rules_bulk_schema'; +import { updateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/update_rules_bulk_schema'; import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; -import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; @@ -25,16 +20,13 @@ import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '.. import { updateRules } from '../../rules/update_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; -import { PartialFilter } from '../../types'; export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => { router.put( { path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, validate: { - body: buildRouteValidation( - updateRulesBulkSchema - ), + body: buildRouteValidation(updateRulesBulkSchema), }, options: { tags: ['access:securitySolution'], @@ -61,138 +53,33 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await Promise.all( request.body.map(async (payloadRule) => { - const { - actions: actionsRest, - anomaly_threshold: anomalyThreshold, - author, - building_block_type: buildingBlockType, - description, - enabled, - event_category_override: eventCategoryOverride, - false_positives: falsePositives, - from, - query: queryOrUndefined, - language: languageOrUndefined, - license, - machine_learning_job_id: machineLearningJobId, - output_index: outputIndex, - saved_id: savedId, - timeline_id: timelineId, - timeline_title: timelineTitle, - meta, - filters: filtersRest, - rule_id: ruleId, - id, - index, - 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_query: threatQuery, - threat_mapping: threatMapping, - threat_language: threatLanguage, - concurrent_searches: concurrentSearches, - items_per_search: itemsPerSearch, - throttle, - timestamp_override: timestampOverride, - references, - note, - version, - exceptions_list: exceptionsList, - } = payloadRule; - const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); - const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; + const idOrRuleIdOrUnknown = payloadRule.id ?? payloadRule.rule_id ?? '(unknown id)'; try { const validationErrors = updateRuleValidateTypeDependents(payloadRule); if (validationErrors.length) { return createBulkErrorObject({ - ruleId, + ruleId: payloadRule.rule_id, statusCode: 400, message: validationErrors.join(), }); } - 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 actions: RuleAlertAction[] = actionsRest as RuleAlertAction[]; - const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; - - throwHttpError(await mlAuthz.validateRuleType(type)); + throwHttpError(await mlAuthz.validateRuleType(payloadRule.type)); const rule = await updateRules({ alertsClient, - anomalyThreshold, - author, - buildingBlockType, - description, - enabled, - eventCategoryOverride, - falsePositives, - from, - query, - language, - license, - machineLearningJobId, - outputIndex: finalIndex, - savedId, savedObjectsClient, - timelineId, - timelineTitle, - meta, - filters, - id, - ruleId, - index, - interval, - maxSignals, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - severity, - severityMapping, - tags, - to, - type, - threat, - threshold, - threatFilters, - threatIndex, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - timestampOverride, - references, - note, - version, - exceptionsList, - actions, + siemClient, + ruleUpdate: payloadRule, }); if (rule != null) { const ruleActions = await updateRulesNotifications({ ruleAlertId: rule.id, alertsClient, savedObjectsClient, - enabled, - actions, - throttle, + enabled: payloadRule.enabled ?? true, + actions: payloadRule.actions, + throttle: payloadRule.throttle, name, }); const ruleStatuses = await ruleStatusClient.find({ @@ -204,7 +91,7 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => }); return transformValidateBulkError(rule.id, rule, ruleActions, ruleStatuses); } 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/update_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index fd077c18b7983e..f70afc8e59ee8d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -160,9 +160,7 @@ describe('update_rules', () => { }); const result = await server.validate(request); - expect(result.badRequest).toHaveBeenCalledWith( - 'Invalid value "unknown type" supplied to "type"' - ); + expect(result.badRequest).toHaveBeenCalled(); }); test('allows rule type of query and custom from and interval', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts index 7ad525b67f7aa1..9f8ba5c129c7ad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -4,13 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { fullUpdateSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; import { updateRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/update_rules_type_dependents'; -import { RuleAlertAction } from '../../../../../common/detection_engine/types'; -import { - updateRulesSchema, - UpdateRulesSchemaDecoded, -} from '../../../../../common/detection_engine/schemas/request/update_rules_schema'; -import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; @@ -23,16 +18,13 @@ import { updateRules } from '../../rules/update_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import { PartialFilter } from '../../types'; export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { router.put( { path: DETECTION_ENGINE_RULES_URL, validate: { - body: buildRouteValidation( - updateRulesSchema - ), + body: buildRouteValidation(fullUpdateSchema), }, options: { tags: ['access:securitySolution'], @@ -44,67 +36,7 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { if (validationErrors.length) { return siemResponse.error({ statusCode: 400, body: validationErrors }); } - - const { - actions: actionsRest, - anomaly_threshold: anomalyThreshold, - author, - building_block_type: buildingBlockType, - description, - enabled, - event_category_override: eventCategoryOverride, - false_positives: falsePositives, - from, - query: queryOrUndefined, - language: languageOrUndefined, - license, - machine_learning_job_id: machineLearningJobId, - output_index: outputIndex, - saved_id: savedId, - timeline_id: timelineId, - timeline_title: timelineTitle, - meta, - filters: filtersRest, - rule_id: ruleId, - id, - index, - 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_query: threatQuery, - threat_mapping: threatMapping, - threat_language: threatLanguage, - concurrent_searches: concurrentSearches, - items_per_search: itemsPerSearch, - throttle, - timestamp_override: timestampOverride, - references, - note, - version, - exceptions_list: exceptionsList, - } = request.body; try { - 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 actions: RuleAlertAction[] = actionsRest as RuleAlertAction[]; - const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; - const alertsClient = context.alerting?.getAlertsClient(); const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.securitySolution?.getAppClient(); @@ -120,59 +52,13 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { request, savedObjectsClient, }); - throwHttpError(await mlAuthz.validateRuleType(type)); + throwHttpError(await mlAuthz.validateRuleType(request.body.type)); - const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const rule = await updateRules({ alertsClient, - anomalyThreshold, - author, - buildingBlockType, - description, - enabled, - eventCategoryOverride, - falsePositives, - from, - query, - language, - license, - machineLearningJobId, - outputIndex: finalIndex, - savedId, savedObjectsClient, - timelineId, - timelineTitle, - meta, - filters, - id, - ruleId, - index, - interval, - maxSignals, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - severity, - severityMapping, - tags, - to, - type, - threat, - threshold, - threatFilters, - threatIndex, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - timestampOverride, - references, - note, - version, - exceptionsList, - actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is rule, otherwise we are a notification and should not enable it + siemClient, + ruleUpdate: request.body, }); if (rule != null) { @@ -180,9 +66,9 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { ruleAlertId: rule.id, alertsClient, savedObjectsClient, - enabled, - actions, - throttle, + enabled: request.body.enabled ?? true, + actions: request.body.actions, + throttle: request.body.throttle, name, }); const ruleStatuses = await ruleStatusClient.find({ @@ -203,7 +89,7 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { return response.ok({ body: validated ?? {} }); } } else { - const error = getIdError({ id, ruleId }); + const error = getIdError({ id: request.body.id, ruleId: request.body.rule_id }); return siemResponse.error({ body: error.message, statusCode: error.statusCode, 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 f6ab3fb0c3ed2e..9130b27f902431 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 @@ -13,6 +13,7 @@ import { SavedObjectsFindResponse, SavedObjectsClientContract, } from 'kibana/server'; +import { FullUpdateSchema } from '../../../../common/detection_engine/schemas/request/rule_schemas'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { FalsePositives, @@ -101,6 +102,7 @@ import { Alert, SanitizedAlert } from '../../../../../alerts/common'; import { SIGNALS_ID } from '../../../../common/constants'; import { RuleTypeParams, PartialFilter } from '../types'; import { ListArrayOrUndefined, ListArray } from '../../../../common/detection_engine/schemas/types'; +import { AppClient } from '../../../types'; export interface RuleAlertType extends Alert { params: RuleTypeParams; @@ -250,55 +252,10 @@ export interface CreateRulesOptions { } export interface UpdateRulesOptions { - id: IdOrUndefined; savedObjectsClient: SavedObjectsClientContract; alertsClient: AlertsClient; - anomalyThreshold: AnomalyThresholdOrUndefined; - author: Author; - buildingBlockType: BuildingBlockTypeOrUndefined; - description: Description; - enabled: Enabled; - eventCategoryOverride: EventCategoryOverrideOrUndefined; - falsePositives: FalsePositives; - from: From; - query: QueryOrUndefined; - language: LanguageOrUndefined; - savedId: SavedIdOrUndefined; - timelineId: TimelineIdOrUndefined; - timelineTitle: TimelineTitleOrUndefined; - meta: MetaOrUndefined; - machineLearningJobId: MachineLearningJobIdOrUndefined; - filters: PartialFilter[]; - ruleId: RuleIdOrUndefined; - index: IndexOrUndefined; - interval: Interval; - license: LicenseOrUndefined; - maxSignals: MaxSignals; - riskScore: RiskScore; - riskScoreMapping: RiskScoreMapping; - ruleNameOverride: RuleNameOverrideOrUndefined; - outputIndex: OutputIndex; - name: Name; - severity: Severity; - severityMapping: SeverityMapping; - tags: Tags; - threat: Threat; - threshold: ThresholdOrUndefined; - threatFilters: ThreatFiltersOrUndefined; - threatIndex: ThreatIndexOrUndefined; - threatQuery: ThreatQueryOrUndefined; - threatMapping: ThreatMappingOrUndefined; - itemsPerSearch: ItemsPerSearchOrUndefined; - concurrentSearches: ConcurrentSearchesOrUndefined; - threatLanguage: ThreatLanguageOrUndefined; - timestampOverride: TimestampOverrideOrUndefined; - to: To; - type: Type; - references: References; - note: NoteOrUndefined; - version: VersionOrUndefined; - exceptionsList: ListArray; - actions: RuleAlertAction[]; + siemClient: AppClient; + ruleUpdate: FullUpdateSchema; } export interface PatchRulesOptions { 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 5168affca5c624..1c20686bfbe3c8 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 @@ -4,179 +4,96 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable complexity */ + +import { DEFAULT_MAX_SIGNALS, SERVER_APP_ID, SIGNALS_ID } from '../../../../common/constants'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { PartialAlert } from '../../../../../alerts/server'; import { readRules } from './read_rules'; import { UpdateRulesOptions } from './types'; import { addTags } from './add_tags'; -import { calculateVersion } from './utils'; import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; +import { typeSpecificSnakeToCamel } from '../schemas/rule_converters'; +import { InternalRuleCreate } from '../schemas/rule_schemas'; export const updateRules = async ({ alertsClient, - author, - buildingBlockType, savedObjectsClient, - description, - eventCategoryOverride, - falsePositives, - enabled, - query, - language, - license, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - from, - id, - ruleId, - index, - interval, - maxSignals, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - severity, - severityMapping, - tags, - threat, - threshold, - threatFilters, - threatIndex, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - timestampOverride, - to, - type, - references, - version, - note, - exceptionsList, - anomalyThreshold, - machineLearningJobId, - actions, + siemClient, + ruleUpdate, }: UpdateRulesOptions): Promise => { - const rule = await readRules({ alertsClient, ruleId, id }); - if (rule == null) { + const existingRule = await readRules({ + alertsClient, + ruleId: ruleUpdate.rule_id, + id: ruleUpdate.id, + }); + if (existingRule == null) { return null; } - const calculatedVersion = calculateVersion(rule.params.immutable, rule.params.version, { - author, - buildingBlockType, - description, - eventCategoryOverride, - falsePositives, - query, - language, - license, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - from, - index, - interval, - maxSignals, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - severity, - severityMapping, - tags, - threat, - threshold, - threatFilters, - threatIndex, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - timestampOverride, - to, - type, - references, - version, - note, - anomalyThreshold, - machineLearningJobId, - exceptionsList, - }); + const typeSpecificParams = typeSpecificSnakeToCamel(ruleUpdate); + const throttle = ruleUpdate.throttle ?? null; + const newInternalRule: InternalRuleCreate = { + name: ruleUpdate.name, + tags: addTags(ruleUpdate.tags ?? [], existingRule.params.ruleId, false), + alertTypeId: SIGNALS_ID, + consumer: SERVER_APP_ID, + params: { + author: ruleUpdate.author ?? [], + buildingBlockType: ruleUpdate.building_block_type, + description: ruleUpdate.description, + ruleId: existingRule.params.ruleId, + falsePositives: ruleUpdate.false_positives ?? [], + from: ruleUpdate.from ?? 'now-6m', + // Unlike the create route, immutable comes from the existing rule here + immutable: existingRule.params.immutable, + license: ruleUpdate.license, + outputIndex: ruleUpdate.output_index ?? siemClient.getSignalsIndex(), + timelineId: ruleUpdate.timeline_id, + timelineTitle: ruleUpdate.timeline_title, + meta: ruleUpdate.meta, + maxSignals: ruleUpdate.max_signals ?? DEFAULT_MAX_SIGNALS, + riskScore: ruleUpdate.risk_score, + riskScoreMapping: ruleUpdate.risk_score_mapping ?? [], + ruleNameOverride: ruleUpdate.rule_name_override, + severity: ruleUpdate.severity, + severityMapping: ruleUpdate.severity_mapping ?? [], + threat: ruleUpdate.threat ?? [], + timestampOverride: ruleUpdate.timestamp_override, + to: ruleUpdate.to ?? 'now', + references: ruleUpdate.references ?? [], + note: ruleUpdate.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: + ruleUpdate.version ?? existingRule.params.immutable + ? existingRule.params.version + : existingRule.params.version + 1, + exceptionsList: ruleUpdate.exceptions_list ?? [], + ...typeSpecificParams, + }, + schedule: { interval: ruleUpdate.interval ?? '5m' }, + enabled: ruleUpdate.enabled ?? true, + actions: throttle === 'rule' ? (ruleUpdate.actions ?? []).map(transformRuleToAlertAction) : [], + throttle: null, + }; const update = await alertsClient.update({ - id: rule.id, - data: { - tags: addTags(tags, rule.params.ruleId, rule.params.immutable), - name, - schedule: { interval }, - actions: actions.map(transformRuleToAlertAction), - throttle: null, - params: { - author, - buildingBlockType, - description, - ruleId: rule.params.ruleId, - falsePositives, - from, - immutable: rule.params.immutable, - query, - language, - license, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - index, - maxSignals, - riskScore, - riskScoreMapping, - ruleNameOverride, - severity, - severityMapping, - threat, - threshold, - threatFilters, - threatIndex, - threatQuery, - threatMapping, - threatLanguage, - timestampOverride, - to, - type, - references, - note, - version: calculatedVersion, - anomalyThreshold, - machineLearningJobId, - exceptionsList, - }, - }, + id: existingRule.id, + data: newInternalRule, }); - if (rule.enabled && enabled === false) { - await alertsClient.disable({ id: rule.id }); - } else if (!rule.enabled && enabled === true) { - await alertsClient.enable({ id: rule.id }); + if (existingRule.enabled && newInternalRule.enabled === false) { + await alertsClient.disable({ id: existingRule.id }); + } else if (!existingRule.enabled && newInternalRule.enabled === true) { + await alertsClient.enable({ id: existingRule.id }); const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const ruleCurrentStatus = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', - search: rule.id, + search: existingRule.id, searchFields: ['alertId'], }); @@ -189,6 +106,5 @@ export const updateRules = async ({ }); } } - - return { ...update, enabled }; + return { ...update, enabled: newInternalRule.enabled }; }; 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 4c5646eacb06b8..0f76cea85bcbae 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 @@ -46,6 +46,8 @@ export const typeSpecificSnakeToCamel = (params: CreateTypeSpecific): TypeSpecif threatMapping: params.threat_mapping, threatLanguage: params.threat_language, threatIndex: params.threat_index, + concurrentSearches: params.concurrent_searches, + itemsPerSearch: params.items_per_search, }; } case 'query': { @@ -118,6 +120,8 @@ export const typeSpecificCamelToSnake = (params: TypeSpecificRuleParams): Respon threat_mapping: params.threatMapping, threat_language: params.threatLanguage, threat_index: params.threatIndex, + concurrent_searches: params.concurrentSearches, + items_per_search: params.itemsPerSearch, }; } case 'query': { 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 75061d80cbf448..5bf00ced4b14c4 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 @@ -11,6 +11,8 @@ import { threat_mapping, threat_index, threat_query, + concurrentSearchesOrUndefined, + itemsPerSearchOrUndefined, } from '../../../../common/detection_engine/schemas/types/threat_mapping'; import { author, @@ -113,6 +115,8 @@ const threatSpecificRuleParams = t.type({ threatMapping: threat_mapping, threatLanguage: t.union([nonEqlLanguages, t.undefined]), threatIndex: threat_index, + concurrentSearches: concurrentSearchesOrUndefined, + itemsPerSearch: itemsPerSearchOrUndefined, }); const querySpecificRuleParams = t.exact( From b9a0fef8259eb2de94bf893c65d4c62c124ac7f0 Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Mon, 9 Nov 2020 18:40:15 -0500 Subject: [PATCH 08/19] Fix missing name error --- .../detection_engine/routes/rules/update_rules_bulk_route.ts | 2 +- .../lib/detection_engine/routes/rules/update_rules_route.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index df2fecf3dae7ab..cda192e32b5119 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -80,7 +80,7 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => enabled: payloadRule.enabled ?? true, actions: payloadRule.actions, throttle: payloadRule.throttle, - name, + name: payloadRule.name, }); const ruleStatuses = await ruleStatusClient.find({ perPage: 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts index 9f8ba5c129c7ad..17a6118d03a111 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -69,7 +69,7 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { enabled: request.body.enabled ?? true, actions: request.body.actions, throttle: request.body.throttle, - name, + name: request.body.name, }); const ruleStatuses = await ruleStatusClient.find({ perPage: 1, From 78afd08b1c6b5eeb818c95371062d804c39e5bf1 Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Tue, 10 Nov 2020 00:36:29 -0500 Subject: [PATCH 09/19] Fix more tests --- .../request/create_rules_bulk_schema.test.ts | 134 ++++++------------ .../schemas/request/rule_schemas.mock.ts | 108 +++++++++++++- .../schemas/request/rule_schemas.ts | 42 ++++++ .../request/update_rules_bulk_schema.test.ts | 131 ++++++----------- .../routes/rules/update_rules_bulk_route.ts | 2 +- .../routes/rules/update_rules_route.ts | 2 +- .../routes/rules/utils.test.ts | 6 +- .../lib/detection_engine/rules/types.ts | 3 +- .../rules/update_rules.mock.ts | 102 ++----------- .../rules/update_rules.test.ts | 45 +++--- .../detection_engine/rules/update_rules.ts | 4 +- .../tests/generating_signals.ts | 32 ++--- .../detection_engine_api_integration/utils.ts | 3 +- 13 files changed, 283 insertions(+), 331 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts index 5c086511e6760e..0fc4a18e0596c0 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts @@ -4,19 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createRulesBulkSchema, - CreateRulesBulkSchema, - CreateRulesBulkSchemaDecoded, -} from './create_rules_bulk_schema'; +import { createRulesBulkSchema, CreateRulesBulkSchema } from './create_rules_bulk_schema'; import { exactCheck } from '../../../exact_check'; import { foldLeftRight } from '../../../test_utils'; -import { - getCreateRulesSchemaMock, - getCreateRulesSchemaDecodedMock, -} from './create_rules_schema.mock'; import { formatErrors } from '../../../format_errors'; -import { CreateRulesSchema } from './create_rules_schema'; +import { getFullCreateSchemaMock } from './rule_schemas.mock'; // only the basics of testing are here. // see: create_rules_schema.test.ts for the bulk of the validation tests @@ -38,41 +30,41 @@ describe('create_rules_bulk_schema', () => { const decoded = createRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([ - 'Invalid value "undefined" supplied to "description"', - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); + expect(formatErrors(output.errors)).toContain( + 'Invalid value "undefined" supplied to "description"' + ); + expect(formatErrors(output.errors)).toContain( + 'Invalid value "undefined" supplied to "risk_score"' + ); + expect(formatErrors(output.errors)).toContain('Invalid value "undefined" supplied to "name"'); + expect(formatErrors(output.errors)).toContain( + 'Invalid value "undefined" supplied to "severity"' + ); expect(output.schema).toEqual({}); }); test('single array element does validate', () => { - const payload: CreateRulesBulkSchema = [getCreateRulesSchemaMock()]; + const payload: CreateRulesBulkSchema = [getFullCreateSchemaMock()]; const decoded = createRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([getCreateRulesSchemaDecodedMock()]); + expect(output.schema).toEqual(payload); }); test('two array elements do validate', () => { - const payload: CreateRulesBulkSchema = [getCreateRulesSchemaMock(), getCreateRulesSchemaMock()]; + const payload: CreateRulesBulkSchema = [getFullCreateSchemaMock(), getFullCreateSchemaMock()]; const decoded = createRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([ - getCreateRulesSchemaDecodedMock(), - getCreateRulesSchemaDecodedMock(), - ]); + expect(output.schema).toEqual(payload); }); test('single array element with a missing value (risk_score) will not validate', () => { - const singleItem = getCreateRulesSchemaMock(); + const singleItem = getFullCreateSchemaMock(); // @ts-expect-error delete singleItem.risk_score; const payload: CreateRulesBulkSchema = [singleItem]; @@ -87,8 +79,8 @@ describe('create_rules_bulk_schema', () => { }); test('two array elements where the first is valid but the second is invalid (risk_score) will not validate', () => { - const singleItem = getCreateRulesSchemaMock(); - const secondItem = getCreateRulesSchemaMock(); + const singleItem = getFullCreateSchemaMock(); + const secondItem = getFullCreateSchemaMock(); // @ts-expect-error delete secondItem.risk_score; const payload: CreateRulesBulkSchema = [singleItem, secondItem]; @@ -103,8 +95,8 @@ describe('create_rules_bulk_schema', () => { }); test('two array elements where the first is invalid (risk_score) but the second is valid will not validate', () => { - const singleItem = getCreateRulesSchemaMock(); - const secondItem = getCreateRulesSchemaMock(); + const singleItem = getFullCreateSchemaMock(); + const secondItem = getFullCreateSchemaMock(); // @ts-expect-error delete singleItem.risk_score; const payload: CreateRulesBulkSchema = [singleItem, secondItem]; @@ -119,8 +111,8 @@ describe('create_rules_bulk_schema', () => { }); test('two array elements where both are invalid (risk_score) will not validate', () => { - const singleItem = getCreateRulesSchemaMock(); - const secondItem = getCreateRulesSchemaMock(); + const singleItem = getFullCreateSchemaMock(); + const secondItem = getFullCreateSchemaMock(); // @ts-expect-error delete singleItem.risk_score; // @ts-expect-error @@ -137,11 +129,11 @@ describe('create_rules_bulk_schema', () => { }); test('two array elements where the first is invalid (extra key and value) but the second is valid will not validate', () => { - const singleItem: CreateRulesSchema & { madeUpValue: string } = { - ...getCreateRulesSchemaMock(), + const singleItem = { + ...getFullCreateSchemaMock(), madeUpValue: 'something', }; - const secondItem = getCreateRulesSchemaMock(); + const secondItem = getFullCreateSchemaMock(); const payload: CreateRulesBulkSchema = [singleItem, secondItem]; const decoded = createRulesBulkSchema.decode(payload); @@ -152,9 +144,9 @@ describe('create_rules_bulk_schema', () => { }); test('two array elements where the second is invalid (extra key and value) but the first is valid will not validate', () => { - const singleItem: CreateRulesSchema = getCreateRulesSchemaMock(); - const secondItem: CreateRulesSchema & { madeUpValue: string } = { - ...getCreateRulesSchemaMock(), + const singleItem = getFullCreateSchemaMock(); + const secondItem = { + ...getFullCreateSchemaMock(), madeUpValue: 'something', }; const payload: CreateRulesBulkSchema = [singleItem, secondItem]; @@ -167,12 +159,12 @@ describe('create_rules_bulk_schema', () => { }); test('two array elements where both are invalid (extra key and value) will not validate', () => { - const singleItem: CreateRulesSchema & { madeUpValue: string } = { - ...getCreateRulesSchemaMock(), + const singleItem = { + ...getFullCreateSchemaMock(), madeUpValue: 'something', }; - const secondItem: CreateRulesSchema & { madeUpValue: string } = { - ...getCreateRulesSchemaMock(), + const secondItem = { + ...getFullCreateSchemaMock(), madeUpValue: 'something', }; const payload: CreateRulesBulkSchema = [singleItem, secondItem]; @@ -184,30 +176,8 @@ describe('create_rules_bulk_schema', () => { expect(output.schema).toEqual({}); }); - test('The default for "from" will be "now-6m"', () => { - const { from, ...withoutFrom } = getCreateRulesSchemaMock(); - const payload: CreateRulesBulkSchema = [withoutFrom]; - - const decoded = createRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as CreateRulesBulkSchemaDecoded)[0].from).toEqual('now-6m'); - }); - - test('The default for "to" will be "now"', () => { - const { to, ...withoutTo } = getCreateRulesSchemaMock(); - const payload: CreateRulesBulkSchema = [withoutTo]; - - const decoded = createRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as CreateRulesBulkSchemaDecoded)[0].to).toEqual('now'); - }); - test('You cannot set the severity to a value other than low, medium, high, or critical', () => { - const badSeverity = { ...getCreateRulesSchemaMock(), severity: 'madeup' }; + const badSeverity = { ...getFullCreateSchemaMock(), severity: 'madeup' }; const payload = [badSeverity]; const decoded = createRulesBulkSchema.decode(payload); @@ -219,32 +189,30 @@ describe('create_rules_bulk_schema', () => { test('You can set "note" to a string', () => { const payload: CreateRulesBulkSchema = [ - { ...getCreateRulesSchemaMock(), note: '# test markdown' }, + { ...getFullCreateSchemaMock(), note: '# test markdown' }, ]; const decoded = createRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([ - { ...getCreateRulesSchemaDecodedMock(), note: '# test markdown' }, - ]); + expect(output.schema).toEqual(payload); }); test('You can set "note" to an empty string', () => { - const payload: CreateRulesBulkSchema = [{ ...getCreateRulesSchemaMock(), note: '' }]; + const payload: CreateRulesBulkSchema = [{ ...getFullCreateSchemaMock(), note: '' }]; const decoded = createRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([{ ...getCreateRulesSchemaDecodedMock(), note: '' }]); + expect(output.schema).toEqual(payload); }); - test('You can set "note" to anything other than string', () => { + test('You cant set "note" to anything other than string', () => { const payload = [ { - ...getCreateRulesSchemaMock(), + ...getFullCreateSchemaMock(), note: { something: 'some object', }, @@ -259,26 +227,4 @@ describe('create_rules_bulk_schema', () => { ]); expect(output.schema).toEqual({}); }); - - test('The default for "actions" will be an empty array', () => { - const { actions, ...withoutActions } = getCreateRulesSchemaMock(); - const payload: CreateRulesBulkSchema = [withoutActions]; - - const decoded = createRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as CreateRulesBulkSchemaDecoded)[0].actions).toEqual([]); - }); - - test('The default for "throttle" will be null', () => { - const { throttle, ...withoutThrottle } = getCreateRulesSchemaMock(); - const payload: CreateRulesBulkSchema = [withoutThrottle]; - - const decoded = createRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as CreateRulesBulkSchemaDecoded)[0].throttle).toEqual(null); - }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts index 2670edd95c6fb6..8465b295c7395f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts @@ -4,7 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { QueryCreateSchema, SavedQueryCreateSchema, ThreatMatchCreateSchema } from './rule_schemas'; +import { + MachineLearningCreateSchema, + MachineLearningUpdateSchema, + QueryCreateSchema, + QueryUpdateSchema, + SavedQueryCreateSchema, + SavedQueryUpdateSchema, + ThreatMatchCreateSchema, + ThreatMatchUpdateSchema, +} from './rule_schemas'; export const getFullCreateSchemaMock = (ruleId = 'rule-1'): QueryCreateSchema => ({ description: 'Detecting root and admin users', @@ -70,3 +79,100 @@ export const getCreateThreatMatchSchemaMock = (ruleId = 'rule-1'): ThreatMatchCr }, ], }); + +export const getCreateMachineLearningSchemaMock = ( + ruleId = 'rule-1' +): MachineLearningCreateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + risk_score: 55, + rule_id: ruleId, + type: 'machine_learning', + anomaly_threshold: 58, + machine_learning_job_id: 'typical-ml-job-id', +}); + +export const getFullUpdateSchemaMock = ( + id = '04128c15-0d1b-4716-a4c5-46997ac7f3bd' +): QueryUpdateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'query', + risk_score: 55, + language: 'kuery', + id, +}); + +export const getUpdateSavedQuerySchemaMock = ( + id = '04128c15-0d1b-4716-a4c5-46997ac7f3bd' +): SavedQueryUpdateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'saved_query', + saved_id: 'some id', + risk_score: 55, + language: 'kuery', + id, +}); + +export const getUpdateThreatMatchSchemaMock = ( + id = '04128c15-0d1b-4716-a4c5-46997ac7f3bd' +): ThreatMatchUpdateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'threat_match', + risk_score: 55, + language: 'kuery', + id, + threat_query: '*:*', + threat_index: ['list-index'], + threat_mapping: [ + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [ + { + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }, + ], +}); + +export const getUpdateMachineLearningSchemaMock = ( + id = '04128c15-0d1b-4716-a4c5-46997ac7f3bd' +): MachineLearningUpdateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + risk_score: 55, + id, + type: 'machine_learning', + anomaly_threshold: 58, + machine_learning_job_id: 'typical-ml-job-id', +}); 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 1527390abf059d..fbb663b71aac8b 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 @@ -342,6 +342,48 @@ export type MachineLearningCreateSchema = t.TypeOf; +export const eqlUpdateSchema = t.intersection([ + eqlCreateParams, + commonCreateParams, + t.exact(t.partial({ id })), +]); +export type EqlUpdateSchema = t.TypeOf; + +export const threatMatchUpdateSchema = t.intersection([ + threatMatchCreateParams, + commonCreateParams, + t.exact(t.partial({ id })), +]); +export type ThreatMatchUpdateSchema = t.TypeOf; + +export const queryUpdateSchema = t.intersection([ + queryCreateParams, + commonCreateParams, + t.exact(t.partial({ id })), +]); +export type QueryUpdateSchema = t.TypeOf; + +export const savedQueryUpdateSchema = t.intersection([ + savedQueryCreateParams, + commonCreateParams, + t.exact(t.partial({ id })), +]); +export type SavedQueryUpdateSchema = t.TypeOf; + +export const thresholdUpdateSchema = t.intersection([ + thresholdCreateParams, + commonCreateParams, + t.exact(t.partial({ id })), +]); +export type ThresholdUpdateSchema = t.TypeOf; + +export const machineLearningUpdateSchema = t.intersection([ + machineLearningCreateParams, + commonCreateParams, + t.exact(t.partial({ id })), +]); +export type MachineLearningUpdateSchema = t.TypeOf; + const patchTypeSpecific = t.union([ eqlPatchParams, threatMatchPatchParams, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts index a82a3abc8c0acc..34283a3e605ba2 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts @@ -8,11 +8,8 @@ import { updateRulesBulkSchema, UpdateRulesBulkSchema } from './update_rules_bul import { exactCheck } from '../../../exact_check'; import { foldLeftRight } from '../../../test_utils'; import { formatErrors } from '../../../format_errors'; -import { - getUpdateRulesSchemaMock, - getUpdateRulesSchemaDecodedMock, -} from './update_rules_schema.mock'; -import { UpdateRulesSchema } from './update_rules_schema'; +import { getFullUpdateSchemaMock } from './rule_schemas.mock'; +import { FullUpdateSchema } from './rule_schemas'; // only the basics of testing are here. // see: update_rules_schema.test.ts for the bulk of the validation tests @@ -34,41 +31,41 @@ describe('update_rules_bulk_schema', () => { const decoded = updateRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([ - 'Invalid value "undefined" supplied to "description"', - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); + expect(formatErrors(output.errors)).toContain( + 'Invalid value "undefined" supplied to "description"' + ); + expect(formatErrors(output.errors)).toContain( + 'Invalid value "undefined" supplied to "risk_score"' + ); + expect(formatErrors(output.errors)).toContain('Invalid value "undefined" supplied to "name"'); + expect(formatErrors(output.errors)).toContain( + 'Invalid value "undefined" supplied to "severity"' + ); expect(output.schema).toEqual({}); }); test('single array element does validate', () => { - const payload: UpdateRulesBulkSchema = [getUpdateRulesSchemaMock()]; + const payload: UpdateRulesBulkSchema = [getFullUpdateSchemaMock()]; const decoded = updateRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([getUpdateRulesSchemaDecodedMock()]); + expect(output.schema).toEqual(payload); }); test('two array elements do validate', () => { - const payload: UpdateRulesBulkSchema = [getUpdateRulesSchemaMock(), getUpdateRulesSchemaMock()]; + const payload: UpdateRulesBulkSchema = [getFullUpdateSchemaMock(), getFullUpdateSchemaMock()]; const decoded = updateRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([ - getUpdateRulesSchemaDecodedMock(), - getUpdateRulesSchemaDecodedMock(), - ]); + expect(output.schema).toEqual(payload); }); test('single array element with a missing value (risk_score) will not validate', () => { - const singleItem = getUpdateRulesSchemaMock(); + const singleItem = getFullUpdateSchemaMock(); // @ts-expect-error delete singleItem.risk_score; const payload: UpdateRulesBulkSchema = [singleItem]; @@ -83,8 +80,8 @@ describe('update_rules_bulk_schema', () => { }); test('two array elements where the first is valid but the second is invalid (risk_score) will not validate', () => { - const singleItem = getUpdateRulesSchemaMock(); - const secondItem = getUpdateRulesSchemaMock(); + const singleItem = getFullUpdateSchemaMock(); + const secondItem = getFullUpdateSchemaMock(); // @ts-expect-error delete secondItem.risk_score; const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; @@ -99,8 +96,8 @@ describe('update_rules_bulk_schema', () => { }); test('two array elements where the first is invalid (risk_score) but the second is valid will not validate', () => { - const singleItem = getUpdateRulesSchemaMock(); - const secondItem = getUpdateRulesSchemaMock(); + const singleItem = getFullUpdateSchemaMock(); + const secondItem = getFullUpdateSchemaMock(); // @ts-expect-error delete singleItem.risk_score; const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; @@ -115,8 +112,8 @@ describe('update_rules_bulk_schema', () => { }); test('two array elements where both are invalid (risk_score) will not validate', () => { - const singleItem = getUpdateRulesSchemaMock(); - const secondItem = getUpdateRulesSchemaMock(); + const singleItem = getFullUpdateSchemaMock(); + const secondItem = getFullUpdateSchemaMock(); // @ts-expect-error delete singleItem.risk_score; // @ts-expect-error @@ -133,12 +130,12 @@ describe('update_rules_bulk_schema', () => { }); test('two array elements where the first is invalid (extra key and value) but the second is valid will not validate', () => { - const singleItem: UpdateRulesSchema & { madeUpValue: string } = { - ...getUpdateRulesSchemaMock(), + const singleItem: FullUpdateSchema & { madeUpValue: string } = { + ...getFullUpdateSchemaMock(), madeUpValue: 'something', }; - const secondItem = getUpdateRulesSchemaMock(); - const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; + const secondItem = getFullUpdateSchemaMock(); + const payload = [singleItem, secondItem]; const decoded = updateRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); @@ -148,9 +145,9 @@ describe('update_rules_bulk_schema', () => { }); test('two array elements where the second is invalid (extra key and value) but the first is valid will not validate', () => { - const singleItem: UpdateRulesSchema = getUpdateRulesSchemaMock(); - const secondItem: UpdateRulesSchema & { madeUpValue: string } = { - ...getUpdateRulesSchemaMock(), + const singleItem: FullUpdateSchema = getFullUpdateSchemaMock(); + const secondItem: FullUpdateSchema & { madeUpValue: string } = { + ...getFullUpdateSchemaMock(), madeUpValue: 'something', }; const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; @@ -163,12 +160,12 @@ describe('update_rules_bulk_schema', () => { }); test('two array elements where both are invalid (extra key and value) will not validate', () => { - const singleItem: UpdateRulesSchema & { madeUpValue: string } = { - ...getUpdateRulesSchemaMock(), + const singleItem: FullUpdateSchema & { madeUpValue: string } = { + ...getFullUpdateSchemaMock(), madeUpValue: 'something', }; - const secondItem: UpdateRulesSchema & { madeUpValue: string } = { - ...getUpdateRulesSchemaMock(), + const secondItem: FullUpdateSchema & { madeUpValue: string } = { + ...getFullUpdateSchemaMock(), madeUpValue: 'something', }; const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; @@ -180,30 +177,8 @@ describe('update_rules_bulk_schema', () => { expect(output.schema).toEqual({}); }); - test('The default for "from" will be "now-6m"', () => { - const { from, ...withoutFrom } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesBulkSchema = [withoutFrom]; - - const decoded = updateRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as UpdateRulesBulkSchema)[0].from).toEqual('now-6m'); - }); - - test('The default for "to" will be "now"', () => { - const { to, ...withoutTo } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesBulkSchema = [withoutTo]; - - const decoded = updateRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as UpdateRulesBulkSchema)[0].to).toEqual('now'); - }); - test('You cannot set the severity to a value other than low, medium, high, or critical', () => { - const badSeverity = { ...getUpdateRulesSchemaMock(), severity: 'madeup' }; + const badSeverity = { ...getFullUpdateSchemaMock(), severity: 'madeup' }; const payload = [badSeverity]; const decoded = updateRulesBulkSchema.decode(payload); @@ -215,32 +190,30 @@ describe('update_rules_bulk_schema', () => { test('You can set "note" to a string', () => { const payload: UpdateRulesBulkSchema = [ - { ...getUpdateRulesSchemaMock(), note: '# test markdown' }, + { ...getFullUpdateSchemaMock(), note: '# test markdown' }, ]; const decoded = updateRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([ - { ...getUpdateRulesSchemaDecodedMock(), note: '# test markdown' }, - ]); + expect(output.schema).toEqual(payload); }); test('You can set "note" to an empty string', () => { - const payload: UpdateRulesBulkSchema = [{ ...getUpdateRulesSchemaMock(), note: '' }]; + const payload: UpdateRulesBulkSchema = [{ ...getFullUpdateSchemaMock(), note: '' }]; const decoded = updateRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); const output = foldLeftRight(checked); expect(formatErrors(output.errors)).toEqual([]); - expect(output.schema).toEqual([{ ...getUpdateRulesSchemaDecodedMock(), note: '' }]); + expect(output.schema).toEqual(payload); }); - test('You can set "note" to anything other than string', () => { + test('You cant set "note" to anything other than string', () => { const payload = [ { - ...getUpdateRulesSchemaMock(), + ...getFullUpdateSchemaMock(), note: { something: 'some object', }, @@ -255,26 +228,4 @@ describe('update_rules_bulk_schema', () => { ]); expect(output.schema).toEqual({}); }); - - test('The default for "actions" will be an empty array', () => { - const { actions, ...withoutActions } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesBulkSchema = [withoutActions]; - - const decoded = updateRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as UpdateRulesBulkSchema)[0].actions).toEqual([]); - }); - - test('The default for "throttle" will be null', () => { - const { throttle, ...withoutThrottle } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesBulkSchema = [withoutThrottle]; - - const decoded = updateRulesBulkSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const output = foldLeftRight(checked); - expect(formatErrors(output.errors)).toEqual([]); - expect((output.schema as UpdateRulesBulkSchema)[0].throttle).toEqual(null); - }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index cda192e32b5119..5f9789220bc102 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -69,7 +69,7 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => const rule = await updateRules({ alertsClient, savedObjectsClient, - siemClient, + defaultOutputIndex: siemClient.getSignalsIndex(), ruleUpdate: payloadRule, }); if (rule != null) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts index 17a6118d03a111..e37dbb47259bc2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -57,7 +57,7 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { const rule = await updateRules({ alertsClient, savedObjectsClient, - siemClient, + defaultOutputIndex: siemClient.getSignalsIndex(), ruleUpdate: request.body, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts index 13eb7495a898aa..a9fc7ce9082ff9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts @@ -27,10 +27,10 @@ import { PartialAlert } from '../../../../../../alerts/server'; import { SanitizedAlert } from '../../../../../../alerts/server/types'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { RuleAlertType } from '../../rules/types'; -import { CreateRulesBulkSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types/threat_mapping'; +import { CreateRulesBulkSchema } from 'x-pack/plugins/security_solution/common/detection_engine/schemas/request'; type PromiseFromStreams = ImportRulesSchemaDecoded | Error; @@ -548,7 +548,7 @@ describe('utils', () => { { rule_id: 'value3' }, {}, {}, - ] as CreateRulesBulkSchemaDecoded, + ] as CreateRulesBulkSchema, 'rule_id' ); const expected = ['value2', 'value3']; @@ -562,7 +562,7 @@ describe('utils', () => { { rule_id: 'value3' }, {}, {}, - ] as CreateRulesBulkSchemaDecoded, + ] as CreateRulesBulkSchema, 'rule_id' ); const expected: string[] = []; 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 9130b27f902431..41b98769111f19 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 @@ -102,7 +102,6 @@ import { Alert, SanitizedAlert } from '../../../../../alerts/common'; import { SIGNALS_ID } from '../../../../common/constants'; import { RuleTypeParams, PartialFilter } from '../types'; import { ListArrayOrUndefined, ListArray } from '../../../../common/detection_engine/schemas/types'; -import { AppClient } from '../../../types'; export interface RuleAlertType extends Alert { params: RuleTypeParams; @@ -254,7 +253,7 @@ export interface CreateRulesOptions { export interface UpdateRulesOptions { savedObjectsClient: SavedObjectsClientContract; alertsClient: AlertsClient; - siemClient: AppClient; + defaultOutputIndex: string; ruleUpdate: FullUpdateSchema; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts index 34be0f6ad843dc..a40a3d972d1760 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts @@ -7,107 +7,21 @@ import { UpdateRulesOptions } from './types'; import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; +import { + getFullUpdateSchemaMock, + getUpdateMachineLearningSchemaMock, +} from '../../../../common/detection_engine/schemas/request/rule_schemas.mock'; export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({ - author: ['Elastic'], - buildingBlockType: undefined, - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', alertsClient: alertsClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(), - anomalyThreshold: undefined, - description: 'some description', - enabled: true, - eventCategoryOverride: undefined, - falsePositives: ['false positive 1', 'false positive 2'], - from: 'now-6m', - query: 'user.name: root or user.name: admin', - language: 'kuery', - license: 'Elastic License', - savedId: 'savedId-123', - timelineId: 'timelineid-123', - timelineTitle: 'timeline-title-123', - meta: {}, - machineLearningJobId: undefined, - filters: [], - ruleId: undefined, - index: ['index-123'], - interval: '5m', - maxSignals: 100, - riskScore: 80, - riskScoreMapping: [], - ruleNameOverride: undefined, - outputIndex: 'output-1', - name: 'Query with a rule id', - severity: 'high', - severityMapping: [], - tags: [], - threat: [], - threshold: undefined, - threatFilters: undefined, - threatIndex: undefined, - threatQuery: undefined, - threatMapping: undefined, - threatLanguage: undefined, - timestampOverride: undefined, - concurrentSearches: undefined, - itemsPerSearch: undefined, - to: 'now', - type: 'query', - references: ['http://www.example.com'], - note: '# sample markdown', - version: 1, - exceptionsList: [], - actions: [], + defaultOutputIndex: '.siem-signals-default', + ruleUpdate: getFullUpdateSchemaMock(), }); export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({ - author: ['Elastic'], - buildingBlockType: undefined, - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', alertsClient: alertsClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(), - anomalyThreshold: 55, - description: 'some description', - enabled: true, - eventCategoryOverride: undefined, - falsePositives: ['false positive 1', 'false positive 2'], - from: 'now-6m', - query: undefined, - language: undefined, - license: 'Elastic License', - savedId: 'savedId-123', - timelineId: 'timelineid-123', - timelineTitle: 'timeline-title-123', - meta: {}, - machineLearningJobId: 'new_job_id', - filters: [], - ruleId: undefined, - index: ['index-123'], - interval: '5m', - maxSignals: 100, - riskScore: 80, - riskScoreMapping: [], - ruleNameOverride: undefined, - outputIndex: 'output-1', - name: 'Machine Learning Job', - severity: 'high', - severityMapping: [], - tags: [], - threat: [], - threshold: undefined, - threatFilters: undefined, - threatIndex: undefined, - threatQuery: undefined, - threatMapping: undefined, - threatLanguage: undefined, - timestampOverride: undefined, - concurrentSearches: undefined, - itemsPerSearch: undefined, - to: 'now', - type: 'machine_learning', - references: ['http://www.example.com'], - note: '# sample markdown', - version: 1, - exceptionsList: [], - actions: [], + defaultOutputIndex: '.siem-signals-default', + ruleUpdate: getUpdateMachineLearningSchemaMock(), }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts index cf59d2bb5e8c44..31212f4fa9d6bb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts @@ -12,61 +12,54 @@ import { AlertsClientMock } from '../../../../../alerts/server/alerts_client.moc describe('updateRules', () => { it('should call alertsClient.disable if the rule was enabled and enabled is false', async () => { const rulesOptionsMock = getUpdateRulesOptionsMock(); - const ruleOptions = { - ...rulesOptionsMock, - enabled: false, - }; - ((ruleOptions.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue(getResult()); + rulesOptionsMock.ruleUpdate.enabled = false; + ((rulesOptionsMock.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue( + getResult() + ); - await updateRules(ruleOptions); + await updateRules(rulesOptionsMock); - expect(ruleOptions.alertsClient.disable).toHaveBeenCalledWith( + expect(rulesOptionsMock.alertsClient.disable).toHaveBeenCalledWith( expect.objectContaining({ - id: rulesOptionsMock.id, + id: rulesOptionsMock.ruleUpdate.id, }) ); }); it('should call alertsClient.enable if the rule was disabled and enabled is true', async () => { const rulesOptionsMock = getUpdateRulesOptionsMock(); - const ruleOptions = { - ...rulesOptionsMock, - enabled: true, - }; + rulesOptionsMock.ruleUpdate.enabled = true; - ((ruleOptions.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue({ + ((rulesOptionsMock.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue({ ...getResult(), enabled: false, }); - await updateRules(ruleOptions); + await updateRules(rulesOptionsMock); - expect(ruleOptions.alertsClient.enable).toHaveBeenCalledWith( + expect(rulesOptionsMock.alertsClient.enable).toHaveBeenCalledWith( expect.objectContaining({ - id: rulesOptionsMock.id, + id: rulesOptionsMock.ruleUpdate.id, }) ); }); - it('calls the alertsClient with ML params', async () => { + it('calls the alertsClient with params', async () => { const rulesOptionsMock = getUpdateMlRulesOptionsMock(); - const ruleOptions = { - ...rulesOptionsMock, - enabled: true, - }; + rulesOptionsMock.ruleUpdate.enabled = true; - ((ruleOptions.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue( + ((rulesOptionsMock.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue( getMlResult() ); - await updateRules(ruleOptions); + await updateRules(rulesOptionsMock); - expect(ruleOptions.alertsClient.update).toHaveBeenCalledWith( + expect(rulesOptionsMock.alertsClient.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ params: expect.objectContaining({ - anomalyThreshold: rulesOptionsMock.anomalyThreshold, - machineLearningJobId: rulesOptionsMock.machineLearningJobId, + type: 'machine_learning', + severity: 'high', }), }), }) 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 1c20686bfbe3c8..b3854cb6fb8f1b 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 @@ -19,7 +19,7 @@ import { InternalRuleCreate } from '../schemas/rule_schemas'; export const updateRules = async ({ alertsClient, savedObjectsClient, - siemClient, + defaultOutputIndex, ruleUpdate, }: UpdateRulesOptions): Promise => { const existingRule = await readRules({ @@ -48,7 +48,7 @@ export const updateRules = async ({ // Unlike the create route, immutable comes from the existing rule here immutable: existingRule.params.immutable, license: ruleUpdate.license, - outputIndex: ruleUpdate.output_index ?? siemClient.getSignalsIndex(), + outputIndex: ruleUpdate.output_index ?? defaultOutputIndex, timelineId: ruleUpdate.timeline_id, timelineTitle: ruleUpdate.timeline_title, meta: ruleUpdate.meta, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 0cd1c21447dfee..778af8168f08e7 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { QueryCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { DEFAULT_SIGNALS_INDEX } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { @@ -53,7 +53,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should have the specific audit record for _id or none of these tests below will pass', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: `_id:${ID}`, @@ -65,7 +65,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should have recorded the rule_id within the signal', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: `_id:${ID}`, @@ -77,7 +77,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should query and get back expected signal structure using a basic KQL query', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: `_id:${ID}`, @@ -124,7 +124,7 @@ export default ({ getService }: FtrProviderContext) => { it('should query and get back expected signal structure when it is a signal on a signal', async () => { // create a 1 signal from 1 auditbeat record - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), from: '1900-01-01T00:00:00.000Z', query: `_id:${ID}`, @@ -133,7 +133,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, 1); // Run signals on top of that 1 signal which should create a single signal (on top of) a signal - const ruleForSignals: CreateRulesSchema = { + const ruleForSignals: QueryCreateSchema = { ...getSimpleRule(), rule_id: 'signal-on-signal', index: [`${DEFAULT_SIGNALS_INDEX}*`], @@ -209,7 +209,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should have the specific audit record for _id or none of these tests below will pass', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_name_clash'], from: '1900-01-01T00:00:00.000Z', @@ -222,7 +222,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should have recorded the rule_id within the signal', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_name_clash'], from: '1900-01-01T00:00:00.000Z', @@ -235,7 +235,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should query and get back expected signal structure using a basic KQL query', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_name_clash'], from: '1900-01-01T00:00:00.000Z', @@ -278,7 +278,7 @@ export default ({ getService }: FtrProviderContext) => { it('should query and get back expected signal structure when it is a signal on a signal', async () => { // create a 1 signal from 1 auditbeat record - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_name_clash'], from: '1900-01-01T00:00:00.000Z', @@ -288,7 +288,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, 1); // Run signals on top of that 1 signal which should create a single signal (on top of) a signal - const ruleForSignals: CreateRulesSchema = { + const ruleForSignals: QueryCreateSchema = { ...getSimpleRule(), rule_id: 'signal-on-signal', index: [`${DEFAULT_SIGNALS_INDEX}*`], @@ -362,7 +362,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should have the specific audit record for _id or none of these tests below will pass', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_object_clash'], from: '1900-01-01T00:00:00.000Z', @@ -375,7 +375,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should have recorded the rule_id within the signal', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_object_clash'], from: '1900-01-01T00:00:00.000Z', @@ -388,7 +388,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should query and get back expected signal structure using a basic KQL query', async () => { - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_object_clash'], from: '1900-01-01T00:00:00.000Z', @@ -437,7 +437,7 @@ export default ({ getService }: FtrProviderContext) => { it('should query and get back expected signal structure when it is a signal on a signal', async () => { // create a 1 signal from 1 auditbeat record - const rule: CreateRulesSchema = { + const rule: QueryCreateSchema = { ...getSimpleRule(), index: ['signal_object_clash'], from: '1900-01-01T00:00:00.000Z', @@ -447,7 +447,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, 1); // Run signals on top of that 1 signal which should create a single signal (on top of) a signal - const ruleForSignals: CreateRulesSchema = { + const ruleForSignals: QueryCreateSchema = { ...getSimpleRule(), rule_id: 'signal-on-signal', index: [`${DEFAULT_SIGNALS_INDEX}*`], diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 7e4d5675726d17..2223a4406d8e90 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -12,6 +12,7 @@ import { SearchResponse } from 'elasticsearch'; import { FullCreateSchema, FullResponseSchema, + QueryCreateSchema, } from '../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../../plugins/lists/common/constants'; import { @@ -77,7 +78,7 @@ export const removeServerGeneratedPropertiesIncludingRuleId = ( * @param ruleId * @param enabled Enables the rule on creation or not. Defaulted to false to enable it on import */ -export const getSimpleRule = (ruleId = 'rule-1', enabled = true): FullCreateSchema => ({ +export const getSimpleRule = (ruleId = 'rule-1', enabled = true): QueryCreateSchema => ({ name: 'Simple Rule Query', description: 'Simple Rule Query', enabled, From 196e05c465b97999f9a9d89d5fb0379f28761421 Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Tue, 10 Nov 2020 00:47:12 -0500 Subject: [PATCH 10/19] Fix import --- .../server/lib/detection_engine/routes/rules/utils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts index a9fc7ce9082ff9..c839ce935914f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts @@ -30,7 +30,7 @@ import { RuleAlertType } from '../../rules/types'; import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types/threat_mapping'; -import { CreateRulesBulkSchema } from 'x-pack/plugins/security_solution/common/detection_engine/schemas/request'; +import { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request'; type PromiseFromStreams = ImportRulesSchemaDecoded | Error; From 3bdb9004f11279152c1883b4741dbaa169d9a822 Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Tue, 10 Nov 2020 17:46:18 -0500 Subject: [PATCH 11/19] Update patch route with internal schema validation --- .../routes/rules/patch_rules_route.test.ts | 1 + .../lib/detection_engine/rules/patch_rules.ts | 38 +++++++++++++------ .../detection_engine/rules/update_rules.ts | 16 ++++---- .../lib/detection_engine/rules/utils.ts | 9 +++-- .../detection_engine/schemas/rule_schemas.ts | 12 ++++++ 5 files changed, 52 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index a406de593652be..4d1d510e46a095 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -90,6 +90,7 @@ describe('patch_rules', () => { method: 'patch', path: DETECTION_ENGINE_RULES_URL, body: { + type: 'machine_learning', rule_id: 'my-rule-id', anomaly_threshold: 4, machine_learning_job_id: 'some_job_id', 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 22b2593283696d..b50499af3a6f56 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,12 +5,22 @@ */ import { defaults } from 'lodash/fp'; +import { validate } from '../../../../common/validate'; import { PartialAlert } from '../../../../../alerts/server'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { PatchRulesOptions } from './types'; import { addTags } from './add_tags'; -import { calculateVersion, calculateName, calculateInterval } from './utils'; +import { calculateVersion, calculateName, calculateInterval, removeUndefined } from './utils'; import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; +import { internalRuleUpdate } from '../schemas/rule_schemas'; + +class PatchError extends Error { + public readonly statusCode: number; + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + } +} export const patchRules = async ({ alertsClient, @@ -159,18 +169,24 @@ export const patchRules = async ({ } ); + const newRule = { + tags: addTags(tags ?? rule.tags, rule.params.ruleId, rule.params.immutable), + throttle: null, + name: calculateName({ updatedName: name, originalName: rule.name }), + schedule: { + interval: calculateInterval(interval, rule.schedule.interval), + }, + actions: actions?.map(transformRuleToAlertAction) ?? rule.actions, + params: removeUndefined(nextParams), + }; + const [validated, errors] = validate(newRule, internalRuleUpdate); + if (errors != null || validated === null) { + throw new PatchError('Applying patch would create invalid rule', 400); + } + const update = await alertsClient.update({ id: rule.id, - data: { - tags: addTags(tags ?? rule.tags, rule.params.ruleId, rule.params.immutable), - throttle: null, - name: calculateName({ updatedName: name, originalName: rule.name }), - schedule: { - interval: calculateInterval(interval, rule.schedule.interval), - }, - actions: actions?.map(transformRuleToAlertAction) ?? rule.actions, - params: nextParams, - }, + data: validated, }); if (rule.enabled && enabled === false) { 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 b3854cb6fb8f1b..dab8769bcaa651 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,7 +6,7 @@ /* eslint-disable complexity */ -import { DEFAULT_MAX_SIGNALS, SERVER_APP_ID, SIGNALS_ID } from '../../../../common/constants'; +import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { PartialAlert } from '../../../../../alerts/server'; import { readRules } from './read_rules'; @@ -14,7 +14,7 @@ import { UpdateRulesOptions } from './types'; import { addTags } from './add_tags'; import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; import { typeSpecificSnakeToCamel } from '../schemas/rule_converters'; -import { InternalRuleCreate } from '../schemas/rule_schemas'; +import { InternalRuleUpdate } from '../schemas/rule_schemas'; export const updateRules = async ({ alertsClient, @@ -33,11 +33,10 @@ export const updateRules = async ({ const typeSpecificParams = typeSpecificSnakeToCamel(ruleUpdate); const throttle = ruleUpdate.throttle ?? null; - const newInternalRule: InternalRuleCreate = { + const enabled = ruleUpdate.enabled ?? true; + const newInternalRule: InternalRuleUpdate = { name: ruleUpdate.name, tags: addTags(ruleUpdate.tags ?? [], existingRule.params.ruleId, false), - alertTypeId: SIGNALS_ID, - consumer: SERVER_APP_ID, params: { author: ruleUpdate.author ?? [], buildingBlockType: ruleUpdate.building_block_type, @@ -73,7 +72,6 @@ export const updateRules = async ({ ...typeSpecificParams, }, schedule: { interval: ruleUpdate.interval ?? '5m' }, - enabled: ruleUpdate.enabled ?? true, actions: throttle === 'rule' ? (ruleUpdate.actions ?? []).map(transformRuleToAlertAction) : [], throttle: null, }; @@ -83,9 +81,9 @@ export const updateRules = async ({ data: newInternalRule, }); - if (existingRule.enabled && newInternalRule.enabled === false) { + if (existingRule.enabled && enabled === false) { await alertsClient.disable({ id: existingRule.id }); - } else if (!existingRule.enabled && newInternalRule.enabled === true) { + } else if (!existingRule.enabled && enabled === true) { await alertsClient.enable({ id: existingRule.id }); const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); @@ -106,5 +104,5 @@ export const updateRules = async ({ }); } } - return { ...update, enabled: newInternalRule.enabled }; + return { ...update, enabled }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts index 83d9e3fd3e59f8..613e8e474079ca 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts @@ -136,10 +136,7 @@ export const calculateVersion = ( // the version number if only the enabled/disabled flag is being set. Likewise if we get other // properties we are not expecting such as updatedAt we do not to cause a version number bump // on that either. - const removedNullValues = pickBy( - (value: unknown) => value != null, - updateProperties - ); + const removedNullValues = removeUndefined(updateProperties); if (isEmpty(removedNullValues)) { return currentVersion; } else { @@ -147,6 +144,10 @@ export const calculateVersion = ( } }; +export const removeUndefined = (obj: object) => { + return pickBy((value: unknown) => value != null, obj); +}; + export const calculateName = ({ updatedName, originalName, 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 5bf00ced4b14c4..fdf46c1e1d012f 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 @@ -185,6 +185,18 @@ export const internalRuleCreate = t.type({ }); export type InternalRuleCreate = t.TypeOf; +export const internalRuleUpdate = t.type({ + name, + tags, + schedule: t.type({ + interval: t.string, + }), + actions: actionsCamel, + params: ruleParams, + throttle: throttleOrNull, +}); +export type InternalRuleUpdate = t.TypeOf; + export const internalRuleResponse = t.intersection([ internalRuleCreate, t.type({ From d8a2b4170cf95bb71869f50ec469f3673f1700b1 Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Tue, 10 Nov 2020 18:57:53 -0500 Subject: [PATCH 12/19] Reorganize new schema as drop-in replacement for create_rules_schema --- .../request/create_rules_bulk_schema.test.ts | 42 +- .../request/create_rules_bulk_schema.ts | 4 +- .../request/create_rules_schema.mock.ts | 157 -- .../request/create_rules_schema.test.ts | 1724 ----------------- .../schemas/request/create_rules_schema.ts | 177 -- .../create_rules_type_dependents.test.ts | 81 +- .../request/create_rules_type_dependents.ts | 96 +- .../detection_engine/schemas/request/index.ts | 1 - .../schemas/request/rule_schemas.mock.ts | 10 +- .../schemas/request/rule_schemas.test.ts | 238 +-- .../schemas/request/rule_schemas.ts | 4 +- .../update_rules_type_dependents.test.ts | 39 - .../request/update_rules_type_dependents.ts | 87 +- .../detection_engine/rules/api.test.ts | 4 +- .../detection_engine/rules/types.ts | 4 +- .../rules/use_create_rule.test.tsx | 6 +- .../rules/use_create_rule.tsx | 6 +- .../rules/create/helpers.test.ts | 8 +- .../detection_engine/rules/create/index.tsx | 4 +- .../routes/__mocks__/request_responses.ts | 18 +- .../rules/create_rules_bulk_route.test.ts | 12 +- .../routes/rules/create_rules_route.test.ts | 10 +- .../routes/rules/create_rules_route.ts | 9 +- .../rules/patch_rules_bulk_route.test.ts | 2 +- .../rules/update_rules_bulk_route.test.ts | 2 +- .../routes/rules/utils.test.ts | 2 +- .../basic/tests/create_rules.ts | 4 +- .../security_and_spaces/tests/add_actions.ts | 4 +- .../tests/create_exceptions.ts | 10 +- .../security_and_spaces/tests/create_rules.ts | 4 +- .../tests/create_threat_matching.ts | 18 +- .../detection_engine_api_integration/utils.ts | 12 +- 32 files changed, 227 insertions(+), 2572 deletions(-) delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts index 0fc4a18e0596c0..0d70b67f137e8f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts @@ -8,10 +8,10 @@ import { createRulesBulkSchema, CreateRulesBulkSchema } from './create_rules_bul import { exactCheck } from '../../../exact_check'; import { foldLeftRight } from '../../../test_utils'; import { formatErrors } from '../../../format_errors'; -import { getFullCreateSchemaMock } from './rule_schemas.mock'; +import { getCreateRulesSchemaMock } from './rule_schemas.mock'; // only the basics of testing are here. -// see: create_rules_schema.test.ts for the bulk of the validation tests +// see: rule_schemas.test.ts for the bulk of the validation tests // this just wraps createRulesSchema in an array describe('create_rules_bulk_schema', () => { test('can take an empty array and validate it', () => { @@ -44,7 +44,7 @@ describe('create_rules_bulk_schema', () => { }); test('single array element does validate', () => { - const payload: CreateRulesBulkSchema = [getFullCreateSchemaMock()]; + const payload: CreateRulesBulkSchema = [getCreateRulesSchemaMock()]; const decoded = createRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); @@ -54,7 +54,7 @@ describe('create_rules_bulk_schema', () => { }); test('two array elements do validate', () => { - const payload: CreateRulesBulkSchema = [getFullCreateSchemaMock(), getFullCreateSchemaMock()]; + const payload: CreateRulesBulkSchema = [getCreateRulesSchemaMock(), getCreateRulesSchemaMock()]; const decoded = createRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); @@ -64,7 +64,7 @@ describe('create_rules_bulk_schema', () => { }); test('single array element with a missing value (risk_score) will not validate', () => { - const singleItem = getFullCreateSchemaMock(); + const singleItem = getCreateRulesSchemaMock(); // @ts-expect-error delete singleItem.risk_score; const payload: CreateRulesBulkSchema = [singleItem]; @@ -79,8 +79,8 @@ describe('create_rules_bulk_schema', () => { }); test('two array elements where the first is valid but the second is invalid (risk_score) will not validate', () => { - const singleItem = getFullCreateSchemaMock(); - const secondItem = getFullCreateSchemaMock(); + const singleItem = getCreateRulesSchemaMock(); + const secondItem = getCreateRulesSchemaMock(); // @ts-expect-error delete secondItem.risk_score; const payload: CreateRulesBulkSchema = [singleItem, secondItem]; @@ -95,8 +95,8 @@ describe('create_rules_bulk_schema', () => { }); test('two array elements where the first is invalid (risk_score) but the second is valid will not validate', () => { - const singleItem = getFullCreateSchemaMock(); - const secondItem = getFullCreateSchemaMock(); + const singleItem = getCreateRulesSchemaMock(); + const secondItem = getCreateRulesSchemaMock(); // @ts-expect-error delete singleItem.risk_score; const payload: CreateRulesBulkSchema = [singleItem, secondItem]; @@ -111,8 +111,8 @@ describe('create_rules_bulk_schema', () => { }); test('two array elements where both are invalid (risk_score) will not validate', () => { - const singleItem = getFullCreateSchemaMock(); - const secondItem = getFullCreateSchemaMock(); + const singleItem = getCreateRulesSchemaMock(); + const secondItem = getCreateRulesSchemaMock(); // @ts-expect-error delete singleItem.risk_score; // @ts-expect-error @@ -130,10 +130,10 @@ describe('create_rules_bulk_schema', () => { test('two array elements where the first is invalid (extra key and value) but the second is valid will not validate', () => { const singleItem = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), madeUpValue: 'something', }; - const secondItem = getFullCreateSchemaMock(); + const secondItem = getCreateRulesSchemaMock(); const payload: CreateRulesBulkSchema = [singleItem, secondItem]; const decoded = createRulesBulkSchema.decode(payload); @@ -144,9 +144,9 @@ describe('create_rules_bulk_schema', () => { }); test('two array elements where the second is invalid (extra key and value) but the first is valid will not validate', () => { - const singleItem = getFullCreateSchemaMock(); + const singleItem = getCreateRulesSchemaMock(); const secondItem = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), madeUpValue: 'something', }; const payload: CreateRulesBulkSchema = [singleItem, secondItem]; @@ -160,11 +160,11 @@ describe('create_rules_bulk_schema', () => { test('two array elements where both are invalid (extra key and value) will not validate', () => { const singleItem = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), madeUpValue: 'something', }; const secondItem = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), madeUpValue: 'something', }; const payload: CreateRulesBulkSchema = [singleItem, secondItem]; @@ -177,7 +177,7 @@ describe('create_rules_bulk_schema', () => { }); test('You cannot set the severity to a value other than low, medium, high, or critical', () => { - const badSeverity = { ...getFullCreateSchemaMock(), severity: 'madeup' }; + const badSeverity = { ...getCreateRulesSchemaMock(), severity: 'madeup' }; const payload = [badSeverity]; const decoded = createRulesBulkSchema.decode(payload); @@ -189,7 +189,7 @@ describe('create_rules_bulk_schema', () => { test('You can set "note" to a string', () => { const payload: CreateRulesBulkSchema = [ - { ...getFullCreateSchemaMock(), note: '# test markdown' }, + { ...getCreateRulesSchemaMock(), note: '# test markdown' }, ]; const decoded = createRulesBulkSchema.decode(payload); @@ -200,7 +200,7 @@ describe('create_rules_bulk_schema', () => { }); test('You can set "note" to an empty string', () => { - const payload: CreateRulesBulkSchema = [{ ...getFullCreateSchemaMock(), note: '' }]; + const payload: CreateRulesBulkSchema = [{ ...getCreateRulesSchemaMock(), note: '' }]; const decoded = createRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); @@ -212,7 +212,7 @@ describe('create_rules_bulk_schema', () => { test('You cant set "note" to anything other than string', () => { const payload = [ { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), note: { something: 'some object', }, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts index 1b800e7de2df98..81b8bf7abbf786 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts @@ -6,7 +6,7 @@ import * as t from 'io-ts'; -import { fullCreateSchema } from './rule_schemas'; +import { createRulesSchema } from './rule_schemas'; -export const createRulesBulkSchema = t.array(fullCreateSchema); +export const createRulesBulkSchema = t.array(createRulesSchema); export type CreateRulesBulkSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts deleted file mode 100644 index 94dd1215d8026e..00000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CreateRulesSchema, CreateRulesSchemaDecoded } from './create_rules_schema'; -import { DEFAULT_MAX_SIGNALS } from '../../../constants'; - -export const getCreateRulesSchemaMock = (ruleId = 'rule-1'): CreateRulesSchema => ({ - description: 'Detecting root and admin users', - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - severity: 'high', - type: 'query', - risk_score: 55, - language: 'kuery', - rule_id: ruleId, -}); - -export const getCreateMlRulesSchemaMock = (ruleId = 'rule-1') => { - const { query, language, index, ...mlParams } = getCreateRulesSchemaMock(ruleId); - - return { - ...mlParams, - type: 'machine_learning', - anomaly_threshold: 58, - machine_learning_job_id: 'typical-ml-job-id', - }; -}; - -export const getCreateRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => ({ - author: [], - severity_mapping: [], - risk_score_mapping: [], - description: 'Detecting root and admin users', - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - severity: 'high', - type: 'query', - risk_score: 55, - language: 'kuery', - references: [], - actions: [], - enabled: true, - false_positives: [], - from: 'now-6m', - interval: '5m', - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - to: 'now', - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - rule_id: 'rule-1', -}); - -export const getCreateThreatMatchRulesSchemaMock = (ruleId = 'rule-1'): CreateRulesSchema => ({ - description: 'Detecting root and admin users', - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - severity: 'high', - type: 'threat_match', - risk_score: 55, - language: 'kuery', - rule_id: ruleId, - threat_query: '*:*', - threat_index: ['list-index'], - threat_mapping: [ - { - entries: [ - { - field: 'host.name', - value: 'host.name', - type: 'mapping', - }, - ], - }, - ], - threat_filters: [ - { - bool: { - must: [ - { - query_string: { - query: 'host.name: linux', - analyze_wildcard: true, - time_zone: 'Zulu', - }, - }, - ], - filter: [], - should: [], - must_not: [], - }, - }, - ], -}); - -export const getCreateThreatMatchRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => ({ - author: [], - severity_mapping: [], - risk_score_mapping: [], - description: 'Detecting root and admin users', - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - severity: 'high', - type: 'threat_match', - risk_score: 55, - language: 'kuery', - references: [], - actions: [], - enabled: true, - false_positives: [], - from: 'now-6m', - interval: '5m', - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - to: 'now', - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - rule_id: 'rule-1', - threat_query: '*:*', - threat_index: ['list-index'], - threat_mapping: [ - { - entries: [ - { - field: 'host.name', - value: 'host.name', - type: 'mapping', - }, - ], - }, - ], - threat_filters: [ - { - bool: { - must: [ - { - query_string: { - query: 'host.name: linux', - analyze_wildcard: true, - time_zone: 'Zulu', - }, - }, - ], - filter: [], - should: [], - must_not: [], - }, - }, - ], -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts deleted file mode 100644 index 1b6a8d6f27762e..00000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts +++ /dev/null @@ -1,1724 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - createRulesSchema, - CreateRulesSchema, - CreateRulesSchemaDecoded, -} from './create_rules_schema'; -import { exactCheck } from '../../../exact_check'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../../../test_utils'; -import { left } from 'fp-ts/lib/Either'; -import { - getCreateRulesSchemaMock, - getCreateRulesSchemaDecodedMock, - getCreateThreatMatchRulesSchemaMock, - getCreateThreatMatchRulesSchemaDecodedMock, -} from './create_rules_schema.mock'; -import { DEFAULT_MAX_SIGNALS } from '../../../constants'; -import { getListArrayMock } from '../types/lists.mock'; - -describe('create rules schema', () => { - test('empty objects do not validate', () => { - const payload: Partial = {}; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "description"', - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('made up values do not validate', () => { - const payload: CreateRulesSchema & { madeUp: string } = { - ...getCreateRulesSchemaMock(), - madeUp: 'hi', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeUp"']); - expect(message.schema).toEqual({}); - }); - - test('[rule_id] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "description"', - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name, severity] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name, severity, type] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - interval: '5m', - index: ['index-1'], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { - const payload: CreateRulesSchema = { - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - query: 'some query', - index: ['index-1'], - interval: '5m', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - query: 'some query', - index: ['index-1'], - interval: '5m', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { - const payload: CreateRulesSchema = { - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { - const payload: CreateRulesSchema = { - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { - const payload: CreateRulesSchema = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { - const payload: CreateRulesSchema = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('You can send in an empty array to threat', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - threat: [], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - threat: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threat] does validate', () => { - const payload: CreateRulesSchema = { - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - threat: [ - { - framework: 'someFramework', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - threat: [ - { - framework: 'someFramework', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - throttle: null, - version: 1, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('allows references to be sent as valid', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - references: ['index-1'], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - references: ['index-1'], - }; - expect(message.schema).toEqual(expected); - }); - - test('defaults references to an array if it is not sent in', () => { - const { references, ...noReferences } = getCreateRulesSchemaMock(); - const decoded = createRulesSchema.decode(noReferences); - const checked = exactCheck(noReferences, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - references: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('references cannot be numbers', () => { - const payload: Omit & { references: number[] } = { - ...getCreateRulesSchemaMock(), - references: [5], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "references"']); - expect(message.schema).toEqual({}); - }); - - test('indexes cannot be numbers', () => { - const payload: Omit & { index: number[] } = { - ...getCreateRulesSchemaMock(), - index: [5], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "index"']); - expect(message.schema).toEqual({}); - }); - - test('saved_query type can have filters with it', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - filters: [], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - filters: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('filters cannot be a string', () => { - const payload: Omit & { filters: string } = { - ...getCreateRulesSchemaMock(), - filters: 'some string', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "filters"', - ]); - expect(message.schema).toEqual({}); - }); - - test('language validates with kuery', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - language: 'kuery', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - language: 'kuery', - }; - expect(message.schema).toEqual(expected); - }); - - test('language validates with lucene', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - language: 'lucene', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - language: 'lucene', - }; - expect(message.schema).toEqual(expected); - }); - - test('language does not validate with something made up', () => { - const payload: Omit & { language: string } = { - ...getCreateRulesSchemaMock(), - language: 'something-made-up', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "something-made-up" supplied to "language"', - ]); - expect(message.schema).toEqual({}); - }); - - test('max_signals cannot be negative', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - max_signals: -1, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "max_signals"', - ]); - expect(message.schema).toEqual({}); - }); - - test('max_signals cannot be zero', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - max_signals: 0, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to "max_signals"']); - expect(message.schema).toEqual({}); - }); - - test('max_signals can be 1', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - max_signals: 1, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - max_signals: 1, - }; - expect(message.schema).toEqual(expected); - }); - - test('You can optionally send in an array of tags', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - tags: ['tag_1', 'tag_2'], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - tags: ['tag_1', 'tag_2'], - }; - expect(message.schema).toEqual(expected); - }); - - test('You cannot send in an array of tags that are numbers', () => { - const payload: Omit & { tags: number[] } = { - ...getCreateRulesSchemaMock(), - tags: [0, 1, 2], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "tags"', - 'Invalid value "1" supplied to "tags"', - 'Invalid value "2" supplied to "tags"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of threat that are missing "framework"', () => { - const payload: Omit & { - threat: Array>>; - } = { - ...getCreateRulesSchemaMock(), - threat: [ - { - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,framework"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of threat that are missing "tactic"', () => { - const payload: Omit & { - threat: Array>>; - } = { - ...getCreateRulesSchemaMock(), - threat: [ - { - framework: 'fake', - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,tactic"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of threat that are missing "technique"', () => { - const payload: Omit & { - threat: Array>>; - } = { - ...getCreateRulesSchemaMock(), - threat: [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - }, - ], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,technique"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You can optionally send in an array of false positives', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - false_positives: ['false_1', 'false_2'], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - false_positives: ['false_1', 'false_2'], - }; - expect(message.schema).toEqual(expected); - }); - - test('You cannot send in an array of false positives that are numbers', () => { - const payload: Omit & { false_positives: number[] } = { - ...getCreateRulesSchemaMock(), - false_positives: [5, 4], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "false_positives"', - 'Invalid value "4" supplied to "false_positives"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot set the immutable to a number when trying to create a rule', () => { - const payload: Omit & { immutable: number } = { - ...getCreateRulesSchemaMock(), - immutable: 5, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "immutable"']); - expect(message.schema).toEqual({}); - }); - - test('You cannot set the risk_score to 101', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - risk_score: 101, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "101" supplied to "risk_score"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot set the risk_score to -1', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - risk_score: -1, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to "risk_score"']); - expect(message.schema).toEqual({}); - }); - - test('You can set the risk_score to 0', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - risk_score: 0, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - risk_score: 0, - }; - expect(message.schema).toEqual(expected); - }); - - test('You can set the risk_score to 100', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - risk_score: 100, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - risk_score: 100, - }; - expect(message.schema).toEqual(expected); - }); - - test('You can set meta to any object you want', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - meta: { - somethingMadeUp: { somethingElse: true }, - }, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - meta: { - somethingMadeUp: { somethingElse: true }, - }, - }; - expect(message.schema).toEqual(expected); - }); - - test('You cannot create meta as a string', () => { - const payload: Omit & { meta: string } = { - ...getCreateRulesSchemaMock(), - meta: 'should not work', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "should not work" supplied to "meta"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You can omit the query string when filters are present', () => { - const { query, ...noQuery } = getCreateRulesSchemaMock(); - const payload: CreateRulesSchema = { - ...noQuery, - filters: [], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { query: expectedQuery, ...expectedNoQuery } = getCreateRulesSchemaDecodedMock(); - const expected: CreateRulesSchemaDecoded = { - ...expectedNoQuery, - filters: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('validates with timeline_id and timeline_title', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - timeline_id: 'timeline-id', - timeline_title: 'timeline-title', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - timeline_id: 'timeline-id', - timeline_title: 'timeline-title', - }; - expect(message.schema).toEqual(expected); - }); - - test('You cannot set the severity to a value other than low, medium, high, or critical', () => { - const payload: Omit & { severity: string } = { - ...getCreateRulesSchemaMock(), - severity: 'junk', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "junk" supplied to "severity"']); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of actions that are missing "group"', () => { - const payload: Omit = { - ...getCreateRulesSchemaMock(), - actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "actions,group"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of actions that are missing "id"', () => { - const payload: Omit = { - ...getCreateRulesSchemaMock(), - actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "actions,id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of actions that are missing "action_type_id"', () => { - const payload: Omit = { - ...getCreateRulesSchemaMock(), - actions: [{ group: 'group', id: 'id', params: {} }], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "actions,action_type_id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of actions that are missing "params"', () => { - const payload: Omit = { - ...getCreateRulesSchemaMock(), - actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "actions,params"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of actions that are including "actionTypeId"', () => { - const payload: Omit = { - ...getCreateRulesSchemaMock(), - actions: [ - { - group: 'group', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "actions,action_type_id"', - ]); - expect(message.schema).toEqual({}); - }); - - describe('note', () => { - test('You can set note to a string', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - note: '# documentation markdown here', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - note: '# documentation markdown here', - }; - expect(message.schema).toEqual(expected); - }); - - test('You can set note to an empty string', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - note: '', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - ...getCreateRulesSchemaDecodedMock(), - note: '', - }; - expect(message.schema).toEqual(expected); - }); - - test('You cannot create note as an object', () => { - const payload: Omit & { note: {} } = { - ...getCreateRulesSchemaMock(), - note: { - somethingHere: 'something else', - }, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "{"somethingHere":"something else"}" supplied to "note"', - ]); - expect(message.schema).toEqual({}); - }); - - test('empty name is not valid', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - name: '', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "name"']); - expect(message.schema).toEqual({}); - }); - - test('empty description is not valid', () => { - const payload: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - description: '', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "" supplied to "description"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note] does validate', () => { - const payload: CreateRulesSchema = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - }); - - test('defaults interval to 5 min', () => { - const { interval, ...noInterval } = getCreateRulesSchemaMock(); - const payload: CreateRulesSchema = { - ...noInterval, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { interval: expectedInterval, ...expectedNoInterval } = getCreateRulesSchemaDecodedMock(); - const expected: CreateRulesSchemaDecoded = { - ...expectedNoInterval, - interval: '5m', - }; - expect(message.schema).toEqual(expected); - }); - - test('defaults max signals to 100', () => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { max_signals, ...noMaxSignals } = getCreateRulesSchemaMock(); - const payload: CreateRulesSchema = { - ...noMaxSignals, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { - max_signals: expectedMaxSignals, - ...expectedNoMaxSignals - } = getCreateRulesSchemaDecodedMock(); - const expected: CreateRulesSchemaDecoded = { - ...expectedNoMaxSignals, - max_signals: 100, - }; - expect(message.schema).toEqual(expected); - }); - - test('The default for "from" will be "now-6m"', () => { - const { from, ...noFrom } = getCreateRulesSchemaMock(); - const payload: CreateRulesSchema = { - ...noFrom, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { from: expectedFrom, ...expectedNoFrom } = getCreateRulesSchemaDecodedMock(); - const expected: CreateRulesSchemaDecoded = { - ...expectedNoFrom, - from: 'now-6m', - }; - expect(message.schema).toEqual(expected); - }); - - test('The default for "to" will be "now"', () => { - const { to, ...noTo } = getCreateRulesSchemaMock(); - const payload: CreateRulesSchema = { - ...noTo, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { to: expectedTo, ...expectedNoTo } = getCreateRulesSchemaDecodedMock(); - const expected: CreateRulesSchemaDecoded = { - ...expectedNoTo, - to: 'now', - }; - expect(message.schema).toEqual(expected); - }); - - test('The default for "actions" will be an empty array', () => { - const { actions, ...noActions } = getCreateRulesSchemaMock(); - const payload: CreateRulesSchema = { - ...noActions, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { actions: expectedActions, ...expectedNoActions } = getCreateRulesSchemaDecodedMock(); - const expected: CreateRulesSchemaDecoded = { - ...expectedNoActions, - actions: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('The default for "throttle" will be null', () => { - const { throttle, ...noThrottle } = getCreateRulesSchemaMock(); - const payload: CreateRulesSchema = { - ...noThrottle, - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { throttle: expectedThrottle, ...expectedNoThrottle } = getCreateRulesSchemaDecodedMock(); - const expected: CreateRulesSchemaDecoded = { - ...expectedNoThrottle, - throttle: null, - }; - expect(message.schema).toEqual(expected); - }); - - test('machine_learning type does validate', () => { - const payload: CreateRulesSchema = { - type: 'machine_learning', - anomaly_threshold: 50, - machine_learning_job_id: 'linux_anomalous_network_activity_ecs', - false_positives: [], - references: [], - risk_score: 50, - threat: [], - name: 'ss', - description: 'ss', - severity: 'low', - tags: [], - interval: '5m', - from: 'now-360s', - to: 'now', - meta: { from: '1m' }, - actions: [], - enabled: true, - throttle: 'no_actions', - rule_id: 'rule-1', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - type: 'machine_learning', - anomaly_threshold: 50, - machine_learning_job_id: 'linux_anomalous_network_activity_ecs', - false_positives: [], - references: [], - risk_score: 50, - threat: [], - name: 'ss', - description: 'ss', - severity: 'low', - tags: [], - interval: '5m', - from: 'now-360s', - to: 'now', - meta: { from: '1m' }, - actions: [], - enabled: true, - throttle: 'no_actions', - exceptions_list: [], - max_signals: DEFAULT_MAX_SIGNALS, - version: 1, - rule_id: 'rule-1', - }; - expect(message.schema).toEqual(expected); - }); - - test('it generates a uuid v4 whenever you omit the rule_id', () => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { rule_id, ...noRuleId } = getCreateRulesSchemaMock(); - const decoded = createRulesSchema.decode(noRuleId); - const checked = exactCheck(noRuleId, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect((message.schema as CreateRulesSchemaDecoded).rule_id).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i - ); - }); - - describe('exception_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and exceptions_list] does validate', () => { - const payload: CreateRulesSchema = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - filters: [], - risk_score: 50, - note: '# some markdown', - exceptions_list: getListArrayMock(), - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - filters: [], - exceptions_list: [ - { - id: 'some_uuid', - list_id: 'list_id_single', - namespace_type: 'single', - type: 'detection', - }, - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { - const payload: CreateRulesSchema = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - filters: [], - risk_score: 50, - note: '# some markdown', - exceptions_list: [], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - filters: [], - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and invalid exceptions_list] does NOT validate', () => { - const payload: Omit & { - exceptions_list: Array<{ id: string; namespace_type: string }>; - } = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - filters: [], - risk_score: 50, - note: '# some markdown', - exceptions_list: [{ id: 'uuid_here', namespace_type: 'not a namespace type' }], - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "exceptions_list,list_id"', - 'Invalid value "undefined" supplied to "exceptions_list,type"', - 'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { - const payload: CreateRulesSchema = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - filters: [], - risk_score: 50, - note: '# some markdown', - }; - - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: CreateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - version: 1, - exceptions_list: [], - filters: [], - }; - expect(message.schema).toEqual(expected); - }); - }); - - describe('threat_mapping', () => { - test('You can set a threat query, index, mapping, filters when creating a rule', () => { - const payload = getCreateThreatMatchRulesSchemaMock(); - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getCreateThreatMatchRulesSchemaDecodedMock(); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('You can set a threat query, index, mapping, filters, concurrent_searches, items_per_search with a when creating a rule', () => { - const payload: CreateRulesSchema = { - ...getCreateThreatMatchRulesSchemaMock(), - concurrent_searches: 10, - items_per_search: 10, - }; - const decoded = createRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected: CreateRulesSchemaDecoded = { - ...getCreateThreatMatchRulesSchemaDecodedMock(), - concurrent_searches: 10, - items_per_search: 10, - }; - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts deleted file mode 100644 index 2fe52bbe470a52..00000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as t from 'io-ts'; - -import { - description, - anomaly_threshold, - building_block_type, - filters, - RuleId, - index, - output_index, - saved_id, - timeline_id, - timeline_title, - meta, - machine_learning_job_id, - risk_score, - MaxSignals, - name, - severity, - Tags, - To, - type, - Threat, - threshold, - ThrottleOrNull, - note, - Version, - References, - Actions, - Enabled, - FalsePositives, - From, - Interval, - language, - query, - license, - rule_name_override, - timestamp_override, - Author, - RiskScoreMapping, - SeverityMapping, - event_category_override, -} from '../common/schemas'; -import { - threat_index, - concurrent_searches, - items_per_search, - threat_query, - threat_filters, - threat_mapping, - threat_language, -} from '../types/threat_mapping'; - -import { - DefaultStringArray, - DefaultActionsArray, - DefaultBooleanTrue, - DefaultFromString, - DefaultIntervalString, - DefaultMaxSignalsNumber, - DefaultToString, - DefaultThreatArray, - DefaultThrottleNull, - DefaultVersionNumber, - DefaultListArray, - ListArray, - DefaultUuid, - DefaultRiskScoreMappingArray, - DefaultSeverityMappingArray, -} from '../types'; - -export const createRulesSchema = t.intersection([ - t.exact( - t.type({ - description, - risk_score, - name, - severity, - type, - }) - ), - t.exact( - t.partial({ - actions: DefaultActionsArray, // defaults to empty actions array if not set during decode - anomaly_threshold, // defaults to undefined if not set during decode - author: DefaultStringArray, // defaults to empty array of strings if not set during decode - building_block_type, // defaults to undefined if not set during decode - enabled: DefaultBooleanTrue, // defaults to true if not set during decode - event_category_override, // defaults to "undefined" if not set during decode - false_positives: DefaultStringArray, // defaults to empty string array if not set during decode - filters, // defaults to undefined if not set during decode - from: DefaultFromString, // defaults to "now-6m" if not set during decode - rule_id: DefaultUuid, - index, // defaults to undefined if not set during decode - interval: DefaultIntervalString, // defaults to "5m" if not set during decode - query, // defaults to undefined if not set during decode - language, // defaults to undefined if not set during decode - license, // defaults to "undefined" if not set during decode - // TODO: output_index: This should be removed eventually - output_index, // defaults to "undefined" if not set during decode - saved_id, // defaults to "undefined" if not set during decode - timeline_id, // defaults to "undefined" if not set during decode - timeline_title, // defaults to "undefined" if not set during decode - meta, // defaults to "undefined" if not set during decode - machine_learning_job_id, // defaults to "undefined" if not set during decode - max_signals: DefaultMaxSignalsNumber, // defaults to DEFAULT_MAX_SIGNALS (100) if not set during decode - risk_score_mapping: DefaultRiskScoreMappingArray, // defaults to empty risk score mapping array if not set during decode - rule_name_override, // defaults to "undefined" if not set during decode - severity_mapping: DefaultSeverityMappingArray, // defaults to empty actions array if not set during decode - tags: DefaultStringArray, // defaults to empty string array if not set during decode - to: DefaultToString, // defaults to "now" if not set during decode - threat: DefaultThreatArray, // defaults to empty array if not set during decode - threshold, // defaults to "undefined" if not set during decode - throttle: DefaultThrottleNull, // defaults to "null" if not set during decode - timestamp_override, // defaults to "undefined" if not set during decode - references: DefaultStringArray, // defaults to empty array of strings if not set during decode - note, // defaults to "undefined" if not set during decode - version: DefaultVersionNumber, // defaults to 1 if not set during decode - exceptions_list: DefaultListArray, // defaults to empty array if not set during decode - threat_mapping, // defaults to "undefined" if not set during decode - threat_query, // defaults to "undefined" if not set during decode - threat_filters, // defaults to "undefined" if not set during decode - threat_index, // defaults to "undefined" if not set during decode - threat_language, // defaults "undefined" if not set during decode - concurrent_searches, // defaults "undefined" if not set during decode - items_per_search, // defaults "undefined" if not set during decode - }) - ), -]); - -export type CreateRulesSchema = t.TypeOf; - -// This type is used after a decode since some things are defaults after a decode. -export type CreateRulesSchemaDecoded = Omit< - CreateRulesSchema, - | 'author' - | 'references' - | 'actions' - | 'enabled' - | 'false_positives' - | 'from' - | 'interval' - | 'max_signals' - | 'risk_score_mapping' - | 'severity_mapping' - | 'tags' - | 'to' - | 'threat' - | 'throttle' - | 'version' - | 'exceptions_list' - | 'rule_id' -> & { - author: Author; - references: References; - actions: Actions; - enabled: Enabled; - false_positives: FalsePositives; - from: From; - interval: Interval; - max_signals: MaxSignals; - risk_score_mapping: RiskScoreMapping; - severity_mapping: SeverityMapping; - tags: Tags; - to: To; - threat: Threat; - throttle: ThrottleOrNull; - version: Version; - exceptions_list: ListArray; - rule_id: RuleId; -}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts index a78b41cd0da187..3c395df03e0f14 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts @@ -4,31 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - getCreateRulesSchemaMock, - getCreateThreatMatchRulesSchemaMock, -} from './create_rules_schema.mock'; -import { CreateRulesSchema } from './create_rules_schema'; +import { getCreateRulesSchemaMock, getCreateThreatMatchRulesSchemaMock } from './rule_schemas.mock'; +import { CreateRulesSchema } from './rule_schemas'; import { createRuleValidateTypeDependents } from './create_rules_type_dependents'; describe('create_rules_type_dependents', () => { - test('saved_id is required when type is saved_query and will not validate without out', () => { - const schema: CreateRulesSchema = { ...getCreateRulesSchemaMock(), type: 'saved_query' }; - delete schema.saved_id; - const errors = createRuleValidateTypeDependents(schema); - expect(errors).toEqual(['when "type" is "saved_query", "saved_id" is required']); - }); - - test('saved_id is required when type is saved_query and validates with it', () => { - const schema: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - type: 'saved_query', - saved_id: '123', - }; - const errors = createRuleValidateTypeDependents(schema); - expect(errors).toEqual([]); - }); - test('You cannot omit timeline_title when timeline_id is present', () => { const schema: CreateRulesSchema = { ...getCreateRulesSchemaMock(), @@ -69,63 +49,6 @@ describe('create_rules_type_dependents', () => { expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']); }); - test('threshold is required when type is threshold and validates with it', () => { - const schema: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - type: 'threshold', - }; - const errors = createRuleValidateTypeDependents(schema); - expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']); - }); - - test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => { - const schema: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - type: 'threshold', - threshold: { - field: '', - value: -1, - }, - }; - const errors = createRuleValidateTypeDependents(schema); - expect(errors).toEqual(['"threshold.value" has to be bigger than 0']); - }); - - test('threat_index, threat_query, and threat_mapping are required when type is "threat_match" and validates with it', () => { - const schema: CreateRulesSchema = { - ...getCreateRulesSchemaMock(), - type: 'threat_match', - }; - const errors = createRuleValidateTypeDependents(schema); - expect(errors).toEqual([ - 'when "type" is "threat_match", "threat_index" is required', - 'when "type" is "threat_match", "threat_query" is required', - 'when "type" is "threat_match", "threat_mapping" is required', - ]); - }); - - test('validates with threat_index, threat_query, and threat_mapping when type is "threat_match"', () => { - const schema = getCreateThreatMatchRulesSchemaMock(); - const { threat_filters: threatFilters, ...noThreatFilters } = schema; - const errors = createRuleValidateTypeDependents(noThreatFilters); - expect(errors).toEqual([]); - }); - - test('does NOT validate when threat_mapping is an empty array', () => { - const schema: CreateRulesSchema = { - ...getCreateThreatMatchRulesSchemaMock(), - threat_mapping: [], - }; - const errors = createRuleValidateTypeDependents(schema); - expect(errors).toEqual(['threat_mapping" must have at least one element']); - }); - - test('validates with threat_index, threat_query, threat_mapping, and an optional threat_filters, when type is "threat_match"', () => { - const schema = getCreateThreatMatchRulesSchemaMock(); - const errors = createRuleValidateTypeDependents(schema); - expect(errors).toEqual([]); - }); - test('validates that both "items_per_search" and "concurrent_searches" works when together', () => { const schema: CreateRulesSchema = { ...getCreateThreatMatchRulesSchemaMock(), diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts index c93b0f0b14f6ad..771540189ad5fc 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts @@ -4,69 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isMlRule } from '../../../machine_learning/helpers'; -import { isThreatMatchRule, isThresholdRule } from '../../utils'; -import { CreateRulesSchema } from './create_rules_schema'; - -export const validateAnomalyThreshold = (rule: CreateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.anomaly_threshold == null) { - return ['when "type" is "machine_learning" anomaly_threshold is required']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateQuery = (rule: CreateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.query != null) { - return ['when "type" is "machine_learning", "query" cannot be set']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateLanguage = (rule: CreateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.language != null) { - return ['when "type" is "machine_learning", "language" cannot be set']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateSavedId = (rule: CreateRulesSchema): string[] => { - if (rule.type === 'saved_query') { - if (rule.saved_id == null) { - return ['when "type" is "saved_query", "saved_id" is required']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateMachineLearningJobId = (rule: CreateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.machine_learning_job_id == null) { - return ['when "type" is "machine_learning", "machine_learning_job_id" is required']; - } else { - return []; - } - } else { - return []; - } -}; +import { CreateRulesSchema } from './rule_schemas'; export const validateTimelineId = (rule: CreateRulesSchema): string[] => { if (rule.timeline_id != null) { @@ -94,33 +32,9 @@ export const validateTimelineTitle = (rule: CreateRulesSchema): string[] => { return []; }; -export const validateThreshold = (rule: CreateRulesSchema): string[] => { - if (isThresholdRule(rule.type)) { - if (!rule.threshold) { - return ['when "type" is "threshold", "threshold" is required']; - } else if (rule.threshold.value <= 0) { - return ['"threshold.value" has to be bigger than 0']; - } else { - return []; - } - } - return []; -}; - export const validateThreatMapping = (rule: CreateRulesSchema): string[] => { let errors: string[] = []; - if (isThreatMatchRule(rule.type)) { - if (rule.threat_mapping == null) { - errors = ['when "type" is "threat_match", "threat_mapping" is required', ...errors]; - } else if (rule.threat_mapping.length === 0) { - errors = ['threat_mapping" must have at least one element', ...errors]; - } - if (rule.threat_query == null) { - errors = ['when "type" is "threat_match", "threat_query" is required', ...errors]; - } - if (rule.threat_index == null) { - errors = ['when "type" is "threat_match", "threat_index" is required', ...errors]; - } + if (rule.type === 'threat_match') { if (rule.concurrent_searches == null && rule.items_per_search != null) { errors = ['when "items_per_search" exists, "concurrent_searches" must also exist', ...errors]; } @@ -133,14 +47,8 @@ export const validateThreatMapping = (rule: CreateRulesSchema): string[] => { export const createRuleValidateTypeDependents = (schema: CreateRulesSchema): string[] => { return [ - ...validateAnomalyThreshold(schema), - ...validateQuery(schema), - ...validateLanguage(schema), - ...validateSavedId(schema), - ...validateMachineLearningJobId(schema), ...validateTimelineId(schema), ...validateTimelineTitle(schema), - ...validateThreshold(schema), ...validateThreatMapping(schema), ]; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts index abfbc391896430..bc32039f9e85f8 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts @@ -5,7 +5,6 @@ */ export * from './add_prepackaged_rules_schema'; export * from './create_rules_bulk_schema'; -export * from './create_rules_schema'; export * from './export_rules_schema'; export * from './find_rules_schema'; export * from './import_rules_schema'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts index 8465b295c7395f..46a33968e230ff 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts @@ -15,7 +15,7 @@ import { ThreatMatchUpdateSchema, } from './rule_schemas'; -export const getFullCreateSchemaMock = (ruleId = 'rule-1'): QueryCreateSchema => ({ +export const getCreateRulesSchemaMock = (ruleId = 'rule-1'): QueryCreateSchema => ({ description: 'Detecting root and admin users', name: 'Query with a rule id', query: 'user.name: root or user.name: admin', @@ -26,7 +26,7 @@ export const getFullCreateSchemaMock = (ruleId = 'rule-1'): QueryCreateSchema => rule_id: ruleId, }); -export const getCreateSavedQuerySchemaMock = (ruleId = 'rule-1'): SavedQueryCreateSchema => ({ +export const getCreateSavedQueryRulesSchemaMock = (ruleId = 'rule-1'): SavedQueryCreateSchema => ({ description: 'Detecting root and admin users', name: 'Query with a rule id', query: 'user.name: root or user.name: admin', @@ -38,7 +38,9 @@ export const getCreateSavedQuerySchemaMock = (ruleId = 'rule-1'): SavedQueryCrea rule_id: ruleId, }); -export const getCreateThreatMatchSchemaMock = (ruleId = 'rule-1'): ThreatMatchCreateSchema => ({ +export const getCreateThreatMatchRulesSchemaMock = ( + ruleId = 'rule-1' +): ThreatMatchCreateSchema => ({ description: 'Detecting root and admin users', name: 'Query with a rule id', query: 'user.name: root or user.name: admin', @@ -80,7 +82,7 @@ export const getCreateThreatMatchSchemaMock = (ruleId = 'rule-1'): ThreatMatchCr ], }); -export const getCreateMachineLearningSchemaMock = ( +export const getCreateMachineLearningRulesSchemaMock = ( ruleId = 'rule-1' ): MachineLearningCreateSchema => ({ description: 'Detecting root and admin users', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts index 7d079cacaaac22..51b984875c921c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fullCreateSchema, FullCreateSchema, SavedQueryCreateSchema } from './rule_schemas'; +import { createRulesSchema, CreateRulesSchema, SavedQueryCreateSchema } from './rule_schemas'; import { exactCheck } from '../../../exact_check'; import { pipe } from 'fp-ts/lib/pipeable'; import { foldLeftRight, getPaths } from '../../../test_utils'; import { left } from 'fp-ts/lib/Either'; import { - getCreateSavedQuerySchemaMock, - getCreateThreatMatchSchemaMock, - getFullCreateSchemaMock, + getCreateSavedQueryRulesSchemaMock, + getCreateThreatMatchRulesSchemaMock, + getCreateRulesSchemaMock, } from './rule_schemas.mock'; import { getListArrayMock } from '../types/lists.mock'; @@ -20,7 +20,7 @@ describe('create rules schema', () => { test('empty objects do not validate', () => { const payload = {}; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(message.errors.length).toBeGreaterThan(0); @@ -28,12 +28,12 @@ describe('create rules schema', () => { }); test('made up values do not validate', () => { - const payload: FullCreateSchema & { madeUp: string } = { - ...getFullCreateSchemaMock(), + const payload: CreateRulesSchema & { madeUp: string } = { + ...getCreateRulesSchemaMock(), madeUp: 'hi', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeUp"']); @@ -45,7 +45,7 @@ describe('create rules schema', () => { rule_id: 'rule-1', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(message.errors.length).toBeGreaterThan(0); @@ -58,7 +58,7 @@ describe('create rules schema', () => { description: 'some description', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(message.errors.length).toBeGreaterThan(0); @@ -72,7 +72,7 @@ describe('create rules schema', () => { from: 'now-5m', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(message.errors.length).toBeGreaterThan(0); @@ -87,7 +87,7 @@ describe('create rules schema', () => { to: 'now', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(message.errors.length).toBeGreaterThan(0); @@ -103,7 +103,7 @@ describe('create rules schema', () => { name: 'some-name', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(message.errors.length).toBeGreaterThan(0); @@ -120,7 +120,7 @@ describe('create rules schema', () => { severity: 'low', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(message.errors.length).toBeGreaterThan(0); @@ -138,7 +138,7 @@ describe('create rules schema', () => { type: 'query', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -159,7 +159,7 @@ describe('create rules schema', () => { type: 'query', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -181,7 +181,7 @@ describe('create rules schema', () => { index: ['index-1'], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -205,7 +205,7 @@ describe('create rules schema', () => { interval: '5m', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -227,7 +227,7 @@ describe('create rules schema', () => { language: 'kuery', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -237,7 +237,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { - const payload: FullCreateSchema = { + const payload: CreateRulesSchema = { rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -252,7 +252,7 @@ describe('create rules schema', () => { language: 'kuery', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -260,7 +260,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { - const payload: FullCreateSchema = { + const payload: CreateRulesSchema = { rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -276,7 +276,7 @@ describe('create rules schema', () => { language: 'kuery', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -284,7 +284,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { - const payload: FullCreateSchema = { + const payload: CreateRulesSchema = { rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -297,7 +297,7 @@ describe('create rules schema', () => { risk_score: 50, }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -305,7 +305,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { - const payload: FullCreateSchema = { + const payload: CreateRulesSchema = { author: [], severity_mapping: [], risk_score_mapping: [], @@ -322,7 +322,7 @@ describe('create rules schema', () => { type: 'query', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -330,12 +330,12 @@ describe('create rules schema', () => { }); test('You can send in an empty array to threat', () => { - const payload: FullCreateSchema = { - ...getFullCreateSchemaMock(), + const payload: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), threat: [], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -343,7 +343,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threat] does validate', () => { - const payload: FullCreateSchema = { + const payload: CreateRulesSchema = { rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -374,7 +374,7 @@ describe('create rules schema', () => { ], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -382,12 +382,12 @@ describe('create rules schema', () => { }); test('allows references to be sent as valid', () => { - const payload: FullCreateSchema = { - ...getFullCreateSchemaMock(), + const payload: CreateRulesSchema = { + ...getCreateRulesSchemaMock(), references: ['index-1'], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -395,12 +395,12 @@ describe('create rules schema', () => { }); test('references cannot be numbers', () => { - const payload: Omit & { references: number[] } = { - ...getFullCreateSchemaMock(), + const payload: Omit & { references: number[] } = { + ...getCreateRulesSchemaMock(), references: [5], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "references"']); @@ -408,12 +408,12 @@ describe('create rules schema', () => { }); test('indexes cannot be numbers', () => { - const payload: Omit & { index: number[] } = { - ...getFullCreateSchemaMock(), + const payload: Omit & { index: number[] } = { + ...getCreateRulesSchemaMock(), index: [5], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "index"']); @@ -422,11 +422,11 @@ describe('create rules schema', () => { test('saved_query type can have filters with it', () => { const payload: SavedQueryCreateSchema = { - ...getCreateSavedQuerySchemaMock(), + ...getCreateSavedQueryRulesSchemaMock(), filters: [], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -435,11 +435,11 @@ describe('create rules schema', () => { test('filters cannot be a string', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), filters: 'some string', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -450,11 +450,11 @@ describe('create rules schema', () => { test('language validates with kuery', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), language: 'kuery', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -463,11 +463,11 @@ describe('create rules schema', () => { test('language validates with lucene', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), language: 'lucene', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -476,11 +476,11 @@ describe('create rules schema', () => { test('language does not validate with something made up', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), language: 'something-made-up', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -491,11 +491,11 @@ describe('create rules schema', () => { test('max_signals cannot be negative', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), max_signals: -1, }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -506,11 +506,11 @@ describe('create rules schema', () => { test('max_signals cannot be zero', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), max_signals: 0, }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to "max_signals"']); @@ -519,11 +519,11 @@ describe('create rules schema', () => { test('max_signals can be 1', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), max_signals: 1, }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -532,11 +532,11 @@ describe('create rules schema', () => { test('You can optionally send in an array of tags', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), tags: ['tag_1', 'tag_2'], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -545,11 +545,11 @@ describe('create rules schema', () => { test('You cannot send in an array of tags that are numbers', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), tags: [0, 1, 2], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -562,7 +562,7 @@ describe('create rules schema', () => { test('You cannot send in an array of threat that are missing "framework"', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), threat: [ { tactic: { @@ -581,7 +581,7 @@ describe('create rules schema', () => { ], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -592,7 +592,7 @@ describe('create rules schema', () => { test('You cannot send in an array of threat that are missing "tactic"', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), threat: [ { framework: 'fake', @@ -607,7 +607,7 @@ describe('create rules schema', () => { ], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -618,7 +618,7 @@ describe('create rules schema', () => { test('You cannot send in an array of threat that are missing "technique"', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), threat: [ { framework: 'fake', @@ -631,7 +631,7 @@ describe('create rules schema', () => { ], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -642,11 +642,11 @@ describe('create rules schema', () => { test('You can optionally send in an array of false positives', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), false_positives: ['false_1', 'false_2'], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -655,11 +655,11 @@ describe('create rules schema', () => { test('You cannot send in an array of false positives that are numbers', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), false_positives: [5, 4], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -671,11 +671,11 @@ describe('create rules schema', () => { test('You cannot set the immutable to a number when trying to create a rule', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), immutable: 5, }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['invalid keys "immutable"']); @@ -684,11 +684,11 @@ describe('create rules schema', () => { test('You cannot set the risk_score to 101', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), risk_score: 101, }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -699,11 +699,11 @@ describe('create rules schema', () => { test('You cannot set the risk_score to -1', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), risk_score: -1, }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to "risk_score"']); @@ -712,11 +712,11 @@ describe('create rules schema', () => { test('You can set the risk_score to 0', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), risk_score: 0, }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -725,11 +725,11 @@ describe('create rules schema', () => { test('You can set the risk_score to 100', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), risk_score: 100, }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -738,13 +738,13 @@ describe('create rules schema', () => { test('You can set meta to any object you want', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), meta: { somethingMadeUp: { somethingElse: true }, }, }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -753,11 +753,11 @@ describe('create rules schema', () => { test('You cannot create meta as a string', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), meta: 'should not work', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -767,13 +767,13 @@ describe('create rules schema', () => { }); test('You can omit the query string when filters are present', () => { - const { query, ...noQuery } = getFullCreateSchemaMock(); + const { query, ...noQuery } = getCreateRulesSchemaMock(); const payload = { ...noQuery, filters: [], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -782,12 +782,12 @@ describe('create rules schema', () => { test('validates with timeline_id and timeline_title', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), timeline_id: 'timeline-id', timeline_title: 'timeline-title', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -796,11 +796,11 @@ describe('create rules schema', () => { test('You cannot set the severity to a value other than low, medium, high, or critical', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), severity: 'junk', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['Invalid value "junk" supplied to "severity"']); @@ -809,11 +809,11 @@ describe('create rules schema', () => { test('You cannot send in an array of actions that are missing "group"', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -824,11 +824,11 @@ describe('create rules schema', () => { test('You cannot send in an array of actions that are missing "id"', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -839,11 +839,11 @@ describe('create rules schema', () => { test('You cannot send in an array of actions that are missing "action_type_id"', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), actions: [{ group: 'group', id: 'id', params: {} }], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -854,11 +854,11 @@ describe('create rules schema', () => { test('You cannot send in an array of actions that are missing "params"', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -869,7 +869,7 @@ describe('create rules schema', () => { test('You cannot send in an array of actions that are including "actionTypeId"', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), actions: [ { group: 'group', @@ -880,7 +880,7 @@ describe('create rules schema', () => { ], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -892,11 +892,11 @@ describe('create rules schema', () => { describe('note', () => { test('You can set note to a string', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), note: '# documentation markdown here', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -905,11 +905,11 @@ describe('create rules schema', () => { test('You can set note to an empty string', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), note: '', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -918,13 +918,13 @@ describe('create rules schema', () => { test('You cannot create note as an object', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), note: { somethingHere: 'something else', }, }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -935,11 +935,11 @@ describe('create rules schema', () => { test('empty name is not valid', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), name: '', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "name"']); @@ -948,11 +948,11 @@ describe('create rules schema', () => { test('empty description is not valid', () => { const payload = { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), description: '', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -976,7 +976,7 @@ describe('create rules schema', () => { note: '# some markdown', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -1007,7 +1007,7 @@ describe('create rules schema', () => { rule_id: 'rule-1', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -1032,7 +1032,7 @@ describe('create rules schema', () => { exceptions_list: getListArrayMock(), }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -1056,7 +1056,7 @@ describe('create rules schema', () => { exceptions_list: [], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -1080,7 +1080,7 @@ describe('create rules schema', () => { exceptions_list: [{ id: 'uuid_here', namespace_type: 'not a namespace type' }], }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ @@ -1107,7 +1107,7 @@ describe('create rules schema', () => { note: '# some markdown', }; - const decoded = fullCreateSchema.decode(payload); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -1117,8 +1117,8 @@ describe('create rules schema', () => { describe('threat_mapping', () => { test('You can set a threat query, index, mapping, filters when creating a rule', () => { - const payload = getCreateThreatMatchSchemaMock(); - const decoded = fullCreateSchema.decode(payload); + const payload = getCreateThreatMatchRulesSchemaMock(); + const decoded = createRulesSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); 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 fbb663b71aac8b..d55b3d2f30be70 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 @@ -339,8 +339,8 @@ export const machineLearningCreateSchema = t.intersection([ ]); export type MachineLearningCreateSchema = t.TypeOf; -export const fullCreateSchema = t.intersection([commonCreateParams, createTypeSpecific]); -export type FullCreateSchema = t.TypeOf; +export const createRulesSchema = t.intersection([commonCreateParams, createTypeSpecific]); +export type CreateRulesSchema = t.TypeOf; export const eqlUpdateSchema = t.intersection([ eqlCreateParams, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts index 91b11ea758e93f..b3bbe6ef891f42 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts @@ -9,23 +9,6 @@ import { UpdateRulesSchema } from './update_rules_schema'; import { updateRuleValidateTypeDependents } from './update_rules_type_dependents'; describe('update_rules_type_dependents', () => { - test('saved_id is required when type is saved_query and will not validate without out', () => { - const schema: UpdateRulesSchema = { ...getUpdateRulesSchemaMock(), type: 'saved_query' }; - delete schema.saved_id; - const errors = updateRuleValidateTypeDependents(schema); - expect(errors).toEqual(['when "type" is "saved_query", "saved_id" is required']); - }); - - test('saved_id is required when type is saved_query and validates with it', () => { - const schema: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - type: 'saved_query', - saved_id: '123', - }; - const errors = updateRuleValidateTypeDependents(schema); - expect(errors).toEqual([]); - }); - test('You cannot omit timeline_title when timeline_id is present', () => { const schema: UpdateRulesSchema = { ...getUpdateRulesSchemaMock(), @@ -85,26 +68,4 @@ describe('update_rules_type_dependents', () => { const errors = updateRuleValidateTypeDependents(schema); expect(errors).toEqual(['either "id" or "rule_id" must be set']); }); - - test('threshold is required when type is threshold and validates with it', () => { - const schema: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - type: 'threshold', - }; - const errors = updateRuleValidateTypeDependents(schema); - expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']); - }); - - test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => { - const schema: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - type: 'threshold', - threshold: { - field: '', - value: -1, - }, - }; - const errors = updateRuleValidateTypeDependents(schema); - expect(errors).toEqual(['"threshold.value" has to be bigger than 0']); - }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts index 5f297fb9688fc5..6f50517cf2470b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts @@ -4,70 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isMlRule } from '../../../machine_learning/helpers'; -import { isThresholdRule } from '../../utils'; import { UpdateRulesSchema } from './update_rules_schema'; -export const validateAnomalyThreshold = (rule: UpdateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.anomaly_threshold == null) { - return ['when "type" is "machine_learning" anomaly_threshold is required']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateQuery = (rule: UpdateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.query != null) { - return ['when "type" is "machine_learning", "query" cannot be set']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateLanguage = (rule: UpdateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.language != null) { - return ['when "type" is "machine_learning", "language" cannot be set']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateSavedId = (rule: UpdateRulesSchema): string[] => { - if (rule.type === 'saved_query') { - if (rule.saved_id == null) { - return ['when "type" is "saved_query", "saved_id" is required']; - } else { - return []; - } - } else { - return []; - } -}; - -export const validateMachineLearningJobId = (rule: UpdateRulesSchema): string[] => { - if (isMlRule(rule.type)) { - if (rule.machine_learning_job_id == null) { - return ['when "type" is "machine_learning", "machine_learning_job_id" is required']; - } else { - return []; - } - } else { - return []; - } -}; - export const validateTimelineId = (rule: UpdateRulesSchema): string[] => { if (rule.timeline_id != null) { if (rule.timeline_title == null) { @@ -104,29 +42,6 @@ export const validateId = (rule: UpdateRulesSchema): string[] => { } }; -export const validateThreshold = (rule: UpdateRulesSchema): string[] => { - if (isThresholdRule(rule.type)) { - if (!rule.threshold) { - return ['when "type" is "threshold", "threshold" is required']; - } else if (rule.threshold.value <= 0) { - return ['"threshold.value" has to be bigger than 0']; - } else { - return []; - } - } - return []; -}; - export const updateRuleValidateTypeDependents = (schema: UpdateRulesSchema): string[] => { - return [ - ...validateId(schema), - ...validateAnomalyThreshold(schema), - ...validateQuery(schema), - ...validateLanguage(schema), - ...validateSavedId(schema), - ...validateMachineLearningJobId(schema), - ...validateTimelineId(schema), - ...validateTimelineTitle(schema), - ...validateThreshold(schema), - ]; + return [...validateId(schema), ...validateTimelineId(schema), ...validateTimelineTitle(schema)]; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index fa93502894699c..a859af789d4a33 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -26,7 +26,7 @@ import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock'; import { rulesMock } from './mock'; import { buildEsQuery } from 'src/plugins/data/common'; -import { getFullCreateSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; jest.mock('../../../../common/lib/kibana'); @@ -42,7 +42,7 @@ describe('Detections Rules API', () => { }); test('POSTs rule', async () => { - const payload = getFullCreateSchemaMock(); + const payload = getCreateRulesSchemaMock(); await createRule({ rule: payload, signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { body: diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index 6e4582e927e7ef..2d68922a048c02 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -6,7 +6,7 @@ import * as t from 'io-ts'; -import { FullCreateSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; +import { CreateRulesSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; import { SortOrder, author, @@ -46,7 +46,7 @@ export const action = t.exact( ); export interface CreateRulesProps { - rule: FullCreateSchema; + rule: CreateRulesSchema; signal: AbortSignal; } diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx index 223396f1dcf8db..0d0047dd06e3f6 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx @@ -7,7 +7,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useCreateRule, ReturnCreateRule } from './use_create_rule'; -import { getFullCreateSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('./api'); @@ -24,7 +24,7 @@ describe('useCreateRule', () => { useCreateRule() ); await waitForNextUpdate(); - result.current[1](getFullCreateSchemaMock()); + result.current[1](getCreateRulesSchemaMock()); rerender(); expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); }); @@ -36,7 +36,7 @@ describe('useCreateRule', () => { useCreateRule() ); await waitForNextUpdate(); - result.current[1](getFullCreateSchemaMock()); + result.current[1](getCreateRulesSchemaMock()); await waitForNextUpdate(); expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx index fecf1dd3816b85..f8ae9e5cc72225 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx @@ -6,7 +6,7 @@ import { useEffect, useState, Dispatch } from 'react'; -import { FullCreateSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; +import { CreateRulesSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { createRule } from './api'; @@ -17,10 +17,10 @@ interface CreateRuleReturn { isSaved: boolean; } -export type ReturnCreateRule = [CreateRuleReturn, Dispatch]; +export type ReturnCreateRule = [CreateRuleReturn, Dispatch]; export const useCreateRule = (): ReturnCreateRule => { - const [rule, setRule] = useState(null); + const [rule, setRule] = useState(null); const [isSaved, setIsSaved] = useState(false); const [isLoading, setIsLoading] = useState(false); const [, dispatchToaster] = useStateToaster(); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts index f79daa2b334519..b37fd40e4418d3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts @@ -5,7 +5,7 @@ */ import { List } from '../../../../../../common/detection_engine/schemas/types'; -import { FullCreateSchema } from '../../../../../../common/detection_engine/schemas/request/rule_schemas'; +import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request/rule_schemas'; import { Rule } from '../../../../containers/detection_engine/rules'; import { getListMock, @@ -762,7 +762,7 @@ describe('helpers', () => { saved_id: '', }, }; - const result: FullCreateSchema = formatRule( + const result: CreateRulesSchema = formatRule( mockDefineStepRuleWithoutSavedId, mockAbout, mockSchedule, @@ -773,14 +773,14 @@ describe('helpers', () => { }); test('returns rule without id if ruleId does not exist', () => { - const result: FullCreateSchema = formatRule( + const result: CreateRulesSchema = formatRule( mockDefine, mockAbout, mockSchedule, mockActions ); - expect(result).not.toHaveProperty('id'); + expect(result).not.toHaveProperty('id'); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index 04d5ac33c4b5bf..e18097a8e29aca 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -16,7 +16,7 @@ import React, { useCallback, useRef, useState, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import styled, { StyledComponent } from 'styled-components'; -import { FullCreateSchema } from '../../../../../../common/detection_engine/schemas/request/rule_schemas'; +import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request/rule_schemas'; import { useCreateRule } from '../../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; @@ -207,7 +207,7 @@ const CreateRulePageComponent: React.FC = () => { stepIsValid(actionsStep) ) { setRule( - formatRule( + formatRule( defineStep.data, aboutStep.data, scheduleStep.data, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 2ed7b383d758f2..f8ba2a9f2d9179 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -28,7 +28,7 @@ import { QuerySignalsSchemaDecoded } from '../../../../../common/detection_engin import { SetSignalsStatusSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { EqlSearchResponse } from '../../../../../common/detection_engine/types'; -import { getFullCreateSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ signal_ids: ['somefakeid1', 'somefakeid2'], @@ -56,14 +56,14 @@ export const getUpdateRequest = () => requestMock.create({ method: 'put', path: DETECTION_ENGINE_RULES_URL, - body: getFullCreateSchemaMock(), + body: getCreateRulesSchemaMock(), }); export const getPatchRequest = () => requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_URL, - body: getFullCreateSchemaMock(), + body: getCreateRulesSchemaMock(), }); export const getReadRequest = () => @@ -83,21 +83,21 @@ export const getReadBulkRequest = () => requestMock.create({ method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - body: [getFullCreateSchemaMock()], + body: [getCreateRulesSchemaMock()], }); export const getUpdateBulkRequest = () => requestMock.create({ method: 'put', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, - body: [getFullCreateSchemaMock()], + body: [getCreateRulesSchemaMock()], }); export const getPatchBulkRequest = () => requestMock.create({ method: 'patch', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, - body: [getFullCreateSchemaMock()], + body: [getCreateRulesSchemaMock()], }); export const getDeleteBulkRequest = () => @@ -233,12 +233,12 @@ export const getCreateRequest = () => requestMock.create({ method: 'post', path: DETECTION_ENGINE_RULES_URL, - body: getFullCreateSchemaMock(), + body: getCreateRulesSchemaMock(), }); // TODO: Replace this with the mocks version from the mocks file export const typicalMlRulePayload = () => { - const { query, language, index, ...mlParams } = getFullCreateSchemaMock(); + const { query, language, index, ...mlParams } = getCreateRulesSchemaMock(); return { ...mlParams, @@ -266,7 +266,7 @@ export const createBulkMlRuleRequest = () => { // TODO: Replace this with a mocks file version export const createRuleWithActionsRequest = () => { - const payload = getFullCreateSchemaMock(); + const payload = getCreateRulesSchemaMock(); return requestMock.create({ method: 'post', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index fe7f0164400b2f..55317fc28afcaf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -18,7 +18,7 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesBulkRoute } from './create_rules_bulk_route'; -import { getFullCreateSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -133,7 +133,7 @@ describe('create_rules_bulk', () => { const request = requestMock.create({ method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - body: [getFullCreateSchemaMock(), getFullCreateSchemaMock()], + body: [getCreateRulesSchemaMock(), getCreateRulesSchemaMock()], }); const response = await server.inject(request, context); @@ -154,7 +154,7 @@ describe('create_rules_bulk', () => { const request = requestMock.create({ method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - body: [{ ...getFullCreateSchemaMock(), type: 'query' }], + body: [{ ...getCreateRulesSchemaMock(), type: 'query' }], }); const result = server.validate(request); @@ -165,7 +165,7 @@ describe('create_rules_bulk', () => { const request = requestMock.create({ method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - body: [{ from: 'now-7m', interval: '5m', ...getFullCreateSchemaMock() }], + body: [{ from: 'now-7m', interval: '5m', ...getCreateRulesSchemaMock() }], }); const result = server.validate(request); @@ -176,7 +176,7 @@ describe('create_rules_bulk', () => { const request = requestMock.create({ method: 'post', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, - body: [{ ...getFullCreateSchemaMock(), type: 'unexpected_type' }], + body: [{ ...getCreateRulesSchemaMock(), type: 'unexpected_type' }], }); const result = server.validate(request); @@ -191,7 +191,7 @@ describe('create_rules_bulk', () => { { from: 'now-3755555555555555.67s', interval: '5m', - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), }, ], }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 9408ad3a1499de..40465f4dc74563 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -20,7 +20,7 @@ import { buildMlAuthz } from '../../../machine_learning/authz'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesRoute } from './create_rules_route'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; -import { getFullCreateSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('../../rules/update_rules_notifications'); jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -140,7 +140,7 @@ describe('create_rules', () => { method: 'post', path: DETECTION_ENGINE_RULES_URL, body: { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), type: 'query', }, }); @@ -154,7 +154,7 @@ describe('create_rules', () => { method: 'post', path: DETECTION_ENGINE_RULES_URL, body: { - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), type: 'unexpected_type', }, }); @@ -167,7 +167,7 @@ describe('create_rules', () => { const request = requestMock.create({ method: 'post', path: DETECTION_ENGINE_RULES_URL, - body: { from: 'now-7m', interval: '5m', ...getFullCreateSchemaMock() }, + body: { from: 'now-7m', interval: '5m', ...getCreateRulesSchemaMock() }, }); const result = server.validate(request); @@ -181,7 +181,7 @@ describe('create_rules', () => { body: { from: 'now-3755555555555555.67s', interval: '5m', - ...getFullCreateSchemaMock(), + ...getCreateRulesSchemaMock(), }, }); const result = server.validate(request); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index cd375735062326..98f567e917bbca 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -26,17 +26,18 @@ import { updateRulesNotifications } from '../../rules/update_rules_notifications import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { addTags } from '../../rules/add_tags'; import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions'; -import { fullCreateSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; +import { createRulesSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; import { newTransformValidate } from './validate'; import { InternalRuleCreate } from '../../schemas/rule_schemas'; import { typeSpecificSnakeToCamel } from '../../schemas/rule_converters'; +import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents'; export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void => { router.post( { path: DETECTION_ENGINE_RULES_URL, validate: { - body: buildRouteValidation(fullCreateSchema), + body: buildRouteValidation(createRulesSchema), }, options: { tags: ['access:securitySolution'], @@ -44,6 +45,10 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); + const validationErrors = createRuleValidateTypeDependents(request.body); + if (validationErrors.length) { + return siemResponse.error({ statusCode: 400, body: validationErrors }); + } try { const alertsClient = context.alerting?.getAlertsClient(); const clusterClient = context.core.elasticsearch.legacy.client; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index c162caa1278e6e..eeb29d99f599cf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -16,7 +16,7 @@ import { } from '../__mocks__/request_responses'; import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { patchRulesBulkRoute } from './patch_rules_bulk_route'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 48071e72ffe6d7..72583a5a787094 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -18,7 +18,7 @@ import { import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { updateRulesBulkRoute } from './update_rules_bulk_route'; import { BulkError } from '../utils'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts index c839ce935914f0..0bd6d43cab4649 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts @@ -28,7 +28,7 @@ import { SanitizedAlert } from '../../../../../../alerts/server/types'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { RuleAlertType } from '../../rules/types'; import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types/threat_mapping'; import { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request'; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts index fda2ebf9e74416..22e3849b686ca1 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { FullCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -66,7 +66,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should create a single rule without an input index', async () => { - const rule: FullCreateSchema = { + const rule: CreateRulesSchema = { name: 'Simple Rule Query', description: 'Simple Rule Query', enabled: true, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts index fd12a5ac02d612..a98b14c3331b81 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { FullCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { @@ -81,7 +81,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); // create a rule with the action attached and a meta field - const ruleWithAction: FullCreateSchema = { + const ruleWithAction: CreateRulesSchema = { ...getRuleWithWebHookAction(hookAction.id), meta: {}, }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts index e5be8d795f92b0..31b1858573586d 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts @@ -7,7 +7,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import expect from '@kbn/expect'; -import { FullCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; import { deleteAllExceptions } from '../../../lists_api_integration/utils'; import { RulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/response'; @@ -62,7 +62,7 @@ export default ({ getService }: FtrProviderContext) => { .send(getCreateExceptionListMinimalSchemaMock()) .expect(200); - const ruleWithException: FullCreateSchema = { + const ruleWithException: CreateRulesSchema = { ...getSimpleRule(), exceptions_list: [ { @@ -99,7 +99,7 @@ export default ({ getService }: FtrProviderContext) => { .send(getCreateExceptionListMinimalSchemaMock()) .expect(200); - const ruleWithException: FullCreateSchema = { + const ruleWithException: CreateRulesSchema = { ...getSimpleRule(), exceptions_list: [ { @@ -421,7 +421,7 @@ export default ({ getService }: FtrProviderContext) => { }; await createExceptionListItem(supertest, exceptionListItem); - const ruleWithException: FullCreateSchema = { + const ruleWithException: CreateRulesSchema = { name: 'Simple Rule Query', description: 'Simple Rule Query', enabled: true, @@ -466,7 +466,7 @@ export default ({ getService }: FtrProviderContext) => { }; await createExceptionListItem(supertest, exceptionListItem); - const ruleWithException: FullCreateSchema = { + const ruleWithException: CreateRulesSchema = { name: 'Simple Rule Query', description: 'Simple Rule Query', enabled: true, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index aee768bc3125ea..d2c7ab6a7db6ff 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { FullCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { DETECTION_ENGINE_RULES_URL, @@ -111,7 +111,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should create a single rule without an input index', async () => { - const rule: FullCreateSchema = { + const rule: CreateRulesSchema = { name: 'Simple Rule Query', description: 'Simple Rule Query', enabled: true, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index 5288f1bc81826a..c6c1013b6f4e5f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { FullCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_RULES_STATUS_URL, @@ -23,7 +23,7 @@ import { waitForSignalsToBePresent, } from '../../utils'; -import { getCreateThreatMatchSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock'; +import { getCreateThreatMatchRulesSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock'; import { getThreatMatchingSchemaPartialMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks'; // eslint-disable-next-line import/no-default-export @@ -41,7 +41,7 @@ export default ({ getService }: FtrProviderContext) => { const { body } = await supertest .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send(getCreateThreatMatchSchemaMock()) + .send(getCreateThreatMatchRulesSchemaMock()) .expect(400); expect(body).to.eql({ @@ -63,13 +63,13 @@ export default ({ getService }: FtrProviderContext) => { }); it('should create a single rule with a rule_id', async () => { - const ruleResponse = await createRule(supertest, getCreateThreatMatchSchemaMock()); + const ruleResponse = await createRule(supertest, getCreateThreatMatchRulesSchemaMock()); const bodyToCompare = removeServerGeneratedProperties(ruleResponse); expect(bodyToCompare).to.eql(getThreatMatchingSchemaPartialMock()); }); it('should create a single rule with a rule_id and validate it ran successfully', async () => { - const ruleResponse = await createRule(supertest, getCreateThreatMatchSchemaMock()); + const ruleResponse = await createRule(supertest, getCreateThreatMatchRulesSchemaMock()); await waitForRuleSuccess(supertest, ruleResponse.id); const { body: statusBody } = await supertest @@ -98,7 +98,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should be able to execute and get 10 signals when doing a specific query', async () => { - const rule: FullCreateSchema = { + const rule: CreateRulesSchema = { description: 'Detecting root and admin users', name: 'Query with a rule id', severity: 'high', @@ -132,7 +132,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return 0 matches if the mapping does not match against anything in the mapping', async () => { - const rule: FullCreateSchema = { + const rule: CreateRulesSchema = { description: 'Detecting root and admin users', name: 'Query with a rule id', severity: 'high', @@ -166,7 +166,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return 0 signals when using an AND and one of the clauses does not have data', async () => { - const rule: FullCreateSchema = { + const rule: CreateRulesSchema = { description: 'Detecting root and admin users', name: 'Query with a rule id', severity: 'high', @@ -204,7 +204,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return 0 signals when using an AND and one of the clauses has a made up value that does not exist', async () => { - const rule: FullCreateSchema = { + const rule: CreateRulesSchema = { description: 'Detecting root and admin users', name: 'Query with a rule id', severity: 'high', diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 2223a4406d8e90..c08d3c1fdd5d6e 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -10,7 +10,7 @@ import supertestAsPromised from 'supertest-as-promised'; import { Context } from '@elastic/elasticsearch/lib/Transport'; import { SearchResponse } from 'elasticsearch'; import { - FullCreateSchema, + CreateRulesSchema, FullResponseSchema, QueryCreateSchema, } from '../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; @@ -109,7 +109,7 @@ export const getSimpleRuleUpdate = (ruleId = 'rule-1'): UpdateRulesSchema => ({ * This is a representative ML rule payload as expected by the server * @param ruleId */ -export const getSimpleMlRule = (ruleId = 'rule-1'): FullCreateSchema => ({ +export const getSimpleMlRule = (ruleId = 'rule-1'): CreateRulesSchema => ({ name: 'Simple ML Rule', description: 'Simple Machine Learning Rule', anomaly_threshold: 44, @@ -189,7 +189,7 @@ export const getSignalStatusEmptyResponse = () => ({ /** * This is a typical simple rule for testing that is easy for most basic testing */ -export const getSimpleRuleWithoutRuleId = (): FullCreateSchema => { +export const getSimpleRuleWithoutRuleId = (): CreateRulesSchema => { const simpleRule = getSimpleRule(); // eslint-disable-next-line @typescript-eslint/naming-convention const { rule_id, ...ruleWithoutId } = simpleRule; @@ -388,7 +388,7 @@ export const getSimpleRuleAsNdjson = (ruleIds: string[], enabled = false): Buffe * testing upload features. * @param rule The rule to convert to ndjson */ -export const ruleToNdjson = (rule: FullCreateSchema): Buffer => { +export const ruleToNdjson = (rule: CreateRulesSchema): Buffer => { const stringified = JSON.stringify(rule); return Buffer.from(`${stringified}\n`); }; @@ -585,7 +585,7 @@ export const getWebHookAction = () => ({ name: 'Some connector', }); -export const getRuleWithWebHookAction = (id: string): FullCreateSchema => ({ +export const getRuleWithWebHookAction = (id: string): CreateRulesSchema => ({ ...getSimpleRule(), throttle: 'rule', actions: [ @@ -728,7 +728,7 @@ export const countDownTest = async ( */ export const createRule = async ( supertest: SuperTest, - rule: FullCreateSchema + rule: CreateRulesSchema ): Promise => { const { body } = await supertest .post(DETECTION_ENGINE_RULES_URL) From 417974ac3f4356443b7a7a9bee03bec856c769b3 Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Tue, 10 Nov 2020 19:38:07 -0500 Subject: [PATCH 13/19] Replace updateRulesSchema with new version --- .../detection_engine/schemas/request/index.ts | 2 +- .../schemas/request/rule_schemas.mock.ts | 2 +- .../schemas/request/rule_schemas.ts | 4 +- .../request/update_rules_bulk_schema.test.ts | 50 +- .../request/update_rules_bulk_schema.ts | 4 +- .../request/update_rules_schema.mock.ts | 45 - .../request/update_rules_schema.test.ts | 1628 ----------------- .../schemas/request/update_rules_schema.ts | 183 -- .../update_rules_type_dependents.test.ts | 4 +- .../request/update_rules_type_dependents.ts | 2 +- .../detection_engine/rules/api.test.ts | 6 +- .../rules/use_update_rule.test.tsx | 2 +- .../rules/patch_rules_bulk_route.test.ts | 1 + .../routes/rules/update_rules_route.test.ts | 2 +- .../routes/rules/update_rules_route.ts | 4 +- .../lib/detection_engine/rules/types.ts | 4 +- .../rules/update_rules.mock.ts | 4 +- .../detection_engine_api_integration/utils.ts | 2 +- 18 files changed, 48 insertions(+), 1901 deletions(-) delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.mock.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts index bc32039f9e85f8..a657191181c0f4 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts @@ -14,4 +14,4 @@ export * from './query_rules_schema'; export * from './query_signals_index_schema'; export * from './set_signal_status_schema'; export * from './update_rules_bulk_schema'; -export * from './update_rules_schema'; +export * from './rule_schemas'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts index 46a33968e230ff..edcf1173c26e25 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts @@ -95,7 +95,7 @@ export const getCreateMachineLearningRulesSchemaMock = ( machine_learning_job_id: 'typical-ml-job-id', }); -export const getFullUpdateSchemaMock = ( +export const getUpdateRulesSchemaMock = ( id = '04128c15-0d1b-4716-a4c5-46997ac7f3bd' ): QueryUpdateSchema => ({ description: 'Detecting root and admin users', 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 d55b3d2f30be70..799b06f169df5a 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 @@ -403,12 +403,12 @@ const responseTypeSpecific = t.union([ ]); export type ResponseTypeSpecific = t.TypeOf; -export const fullUpdateSchema = t.intersection([ +export const updateRulesSchema = t.intersection([ commonCreateParams, createTypeSpecific, t.exact(t.partial({ id })), ]); -export type FullUpdateSchema = t.TypeOf; +export type UpdateRulesSchema = t.TypeOf; export const fullPatchSchema = t.intersection([ commonPatchParams, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts index 34283a3e605ba2..e3ad450eaa8443 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts @@ -8,8 +8,8 @@ import { updateRulesBulkSchema, UpdateRulesBulkSchema } from './update_rules_bul import { exactCheck } from '../../../exact_check'; import { foldLeftRight } from '../../../test_utils'; import { formatErrors } from '../../../format_errors'; -import { getFullUpdateSchemaMock } from './rule_schemas.mock'; -import { FullUpdateSchema } from './rule_schemas'; +import { getUpdateRulesSchemaMock } from './rule_schemas.mock'; +import { UpdateRulesSchema } from './rule_schemas'; // only the basics of testing are here. // see: update_rules_schema.test.ts for the bulk of the validation tests @@ -45,7 +45,7 @@ describe('update_rules_bulk_schema', () => { }); test('single array element does validate', () => { - const payload: UpdateRulesBulkSchema = [getFullUpdateSchemaMock()]; + const payload: UpdateRulesBulkSchema = [getUpdateRulesSchemaMock()]; const decoded = updateRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); @@ -55,7 +55,7 @@ describe('update_rules_bulk_schema', () => { }); test('two array elements do validate', () => { - const payload: UpdateRulesBulkSchema = [getFullUpdateSchemaMock(), getFullUpdateSchemaMock()]; + const payload: UpdateRulesBulkSchema = [getUpdateRulesSchemaMock(), getUpdateRulesSchemaMock()]; const decoded = updateRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); @@ -65,7 +65,7 @@ describe('update_rules_bulk_schema', () => { }); test('single array element with a missing value (risk_score) will not validate', () => { - const singleItem = getFullUpdateSchemaMock(); + const singleItem = getUpdateRulesSchemaMock(); // @ts-expect-error delete singleItem.risk_score; const payload: UpdateRulesBulkSchema = [singleItem]; @@ -80,8 +80,8 @@ describe('update_rules_bulk_schema', () => { }); test('two array elements where the first is valid but the second is invalid (risk_score) will not validate', () => { - const singleItem = getFullUpdateSchemaMock(); - const secondItem = getFullUpdateSchemaMock(); + const singleItem = getUpdateRulesSchemaMock(); + const secondItem = getUpdateRulesSchemaMock(); // @ts-expect-error delete secondItem.risk_score; const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; @@ -96,8 +96,8 @@ describe('update_rules_bulk_schema', () => { }); test('two array elements where the first is invalid (risk_score) but the second is valid will not validate', () => { - const singleItem = getFullUpdateSchemaMock(); - const secondItem = getFullUpdateSchemaMock(); + const singleItem = getUpdateRulesSchemaMock(); + const secondItem = getUpdateRulesSchemaMock(); // @ts-expect-error delete singleItem.risk_score; const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; @@ -112,8 +112,8 @@ describe('update_rules_bulk_schema', () => { }); test('two array elements where both are invalid (risk_score) will not validate', () => { - const singleItem = getFullUpdateSchemaMock(); - const secondItem = getFullUpdateSchemaMock(); + const singleItem = getUpdateRulesSchemaMock(); + const secondItem = getUpdateRulesSchemaMock(); // @ts-expect-error delete singleItem.risk_score; // @ts-expect-error @@ -130,11 +130,11 @@ describe('update_rules_bulk_schema', () => { }); test('two array elements where the first is invalid (extra key and value) but the second is valid will not validate', () => { - const singleItem: FullUpdateSchema & { madeUpValue: string } = { - ...getFullUpdateSchemaMock(), + const singleItem: UpdateRulesSchema & { madeUpValue: string } = { + ...getUpdateRulesSchemaMock(), madeUpValue: 'something', }; - const secondItem = getFullUpdateSchemaMock(); + const secondItem = getUpdateRulesSchemaMock(); const payload = [singleItem, secondItem]; const decoded = updateRulesBulkSchema.decode(payload); @@ -145,9 +145,9 @@ describe('update_rules_bulk_schema', () => { }); test('two array elements where the second is invalid (extra key and value) but the first is valid will not validate', () => { - const singleItem: FullUpdateSchema = getFullUpdateSchemaMock(); - const secondItem: FullUpdateSchema & { madeUpValue: string } = { - ...getFullUpdateSchemaMock(), + const singleItem: UpdateRulesSchema = getUpdateRulesSchemaMock(); + const secondItem: UpdateRulesSchema & { madeUpValue: string } = { + ...getUpdateRulesSchemaMock(), madeUpValue: 'something', }; const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; @@ -160,12 +160,12 @@ describe('update_rules_bulk_schema', () => { }); test('two array elements where both are invalid (extra key and value) will not validate', () => { - const singleItem: FullUpdateSchema & { madeUpValue: string } = { - ...getFullUpdateSchemaMock(), + const singleItem: UpdateRulesSchema & { madeUpValue: string } = { + ...getUpdateRulesSchemaMock(), madeUpValue: 'something', }; - const secondItem: FullUpdateSchema & { madeUpValue: string } = { - ...getFullUpdateSchemaMock(), + const secondItem: UpdateRulesSchema & { madeUpValue: string } = { + ...getUpdateRulesSchemaMock(), madeUpValue: 'something', }; const payload: UpdateRulesBulkSchema = [singleItem, secondItem]; @@ -178,7 +178,7 @@ describe('update_rules_bulk_schema', () => { }); test('You cannot set the severity to a value other than low, medium, high, or critical', () => { - const badSeverity = { ...getFullUpdateSchemaMock(), severity: 'madeup' }; + const badSeverity = { ...getUpdateRulesSchemaMock(), severity: 'madeup' }; const payload = [badSeverity]; const decoded = updateRulesBulkSchema.decode(payload); @@ -190,7 +190,7 @@ describe('update_rules_bulk_schema', () => { test('You can set "note" to a string', () => { const payload: UpdateRulesBulkSchema = [ - { ...getFullUpdateSchemaMock(), note: '# test markdown' }, + { ...getUpdateRulesSchemaMock(), note: '# test markdown' }, ]; const decoded = updateRulesBulkSchema.decode(payload); @@ -201,7 +201,7 @@ describe('update_rules_bulk_schema', () => { }); test('You can set "note" to an empty string', () => { - const payload: UpdateRulesBulkSchema = [{ ...getFullUpdateSchemaMock(), note: '' }]; + const payload: UpdateRulesBulkSchema = [{ ...getUpdateRulesSchemaMock(), note: '' }]; const decoded = updateRulesBulkSchema.decode(payload); const checked = exactCheck(payload, decoded); @@ -213,7 +213,7 @@ describe('update_rules_bulk_schema', () => { test('You cant set "note" to anything other than string', () => { const payload = [ { - ...getFullUpdateSchemaMock(), + ...getUpdateRulesSchemaMock(), note: { something: 'some object', }, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.ts index 1596f995d8b8b7..f1d7c609916055 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.ts @@ -5,7 +5,7 @@ */ import * as t from 'io-ts'; -import { fullUpdateSchema } from './rule_schemas'; +import { updateRulesSchema } from './rule_schemas'; -export const updateRulesBulkSchema = t.array(fullUpdateSchema); +export const updateRulesBulkSchema = t.array(updateRulesSchema); export type UpdateRulesBulkSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.mock.ts deleted file mode 100644 index b3fbf961883528..00000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.mock.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { UpdateRulesSchema, UpdateRulesSchemaDecoded } from './update_rules_schema'; -import { DEFAULT_MAX_SIGNALS } from '../../../constants'; - -export const getUpdateRulesSchemaMock = (): UpdateRulesSchema => ({ - description: 'some description', - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - severity: 'high', - type: 'query', - risk_score: 55, - language: 'kuery', - rule_id: 'rule-1', -}); - -export const getUpdateRulesSchemaDecodedMock = (): UpdateRulesSchemaDecoded => ({ - author: [], - description: 'some description', - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - severity: 'high', - severity_mapping: [], - type: 'query', - risk_score: 55, - risk_score_mapping: [], - language: 'kuery', - references: [], - actions: [], - enabled: true, - false_positives: [], - from: 'now-6m', - interval: '5m', - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - to: 'now', - threat: [], - throttle: null, - exceptions_list: [], - rule_id: 'rule-1', -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts deleted file mode 100644 index e3347b41ac0fa5..00000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts +++ /dev/null @@ -1,1628 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - updateRulesSchema, - UpdateRulesSchema, - UpdateRulesSchemaDecoded, -} from './update_rules_schema'; -import { exactCheck } from '../../../exact_check'; -import { foldLeftRight, getPaths } from '../../../test_utils'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { - getUpdateRulesSchemaMock, - getUpdateRulesSchemaDecodedMock, -} from './update_rules_schema.mock'; -import { DEFAULT_MAX_SIGNALS } from '../../../constants'; -import { getListArrayMock } from '../types/lists.mock'; - -describe('update rules schema', () => { - test('empty objects do not validate', () => { - const payload: Partial = {}; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "description"', - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('made up values do not validate', () => { - const payload: UpdateRulesSchema & { madeUp: string } = { - ...getUpdateRulesSchemaMock(), - madeUp: 'hi', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeUp"']); - expect(message.schema).toEqual({}); - }); - - test('[rule_id] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "description"', - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "name"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('name cannot be an empty string', () => { - const payload: UpdateRulesSchema = { - description: 'some description', - name: '', - risk_score: 50, - severity: 'low', - type: 'query', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "name"']); - expect(message.schema).toEqual({}); - }); - - test('description cannot be an empty string', () => { - const payload: UpdateRulesSchema = { - description: '', - name: 'rule name', - risk_score: 50, - severity: 'low', - type: 'query', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "description"']); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "severity"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name, severity] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - 'Invalid value "undefined" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name, severity, type] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - interval: '5m', - index: ['index-1'], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - query: 'some query', - index: ['index-1'], - interval: '5m', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'low', - type: 'query', - query: 'some query', - index: ['index-1'], - interval: '5m', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { - const payload: Partial = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "risk_score"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('You can send in an empty array to threat', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - threat: [], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - threat: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threat] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - threat: [ - { - framework: 'someFramework', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - threat: [ - { - framework: 'someFramework', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - throttle: null, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('allows references to be sent as valid', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - references: ['index-1'], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - references: ['index-1'], - }; - expect(message.schema).toEqual(expected); - }); - - test('defaults references to an array if it is not sent in', () => { - const { references, ...noReferences } = getUpdateRulesSchemaMock(); - const decoded = updateRulesSchema.decode(noReferences); - const checked = exactCheck(noReferences, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - references: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('references cannot be numbers', () => { - const payload: Omit & { references: number[] } = { - ...getUpdateRulesSchemaMock(), - references: [5], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "references"']); - expect(message.schema).toEqual({}); - }); - - test('indexes cannot be numbers', () => { - const payload: Omit & { index: number[] } = { - ...getUpdateRulesSchemaMock(), - index: [5], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "index"']); - expect(message.schema).toEqual({}); - }); - - test('defaults interval to 5 min', () => { - const { interval, ...noInterval } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesSchema = { - ...noInterval, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { interval: expectedInterval, ...expectedNoInterval } = getUpdateRulesSchemaDecodedMock(); - const expected: UpdateRulesSchemaDecoded = { - ...expectedNoInterval, - interval: '5m', - }; - expect(message.schema).toEqual(expected); - }); - - test('defaults max signals to 100', () => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { max_signals, ...noMaxSignals } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesSchema = { - ...noMaxSignals, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { - max_signals: expectedMaxSignals, - ...expectedNoMaxSignals - } = getUpdateRulesSchemaDecodedMock(); - const expected: UpdateRulesSchemaDecoded = { - ...expectedNoMaxSignals, - max_signals: 100, - }; - expect(message.schema).toEqual(expected); - }); - - test('saved_query type can have filters with it', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - filters: [], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - filters: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('filters cannot be a string', () => { - const payload: Omit & { filters: string } = { - ...getUpdateRulesSchemaMock(), - filters: 'some string', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "filters"', - ]); - expect(message.schema).toEqual({}); - }); - - test('language validates with kuery', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - language: 'kuery', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - language: 'kuery', - }; - expect(message.schema).toEqual(expected); - }); - - test('language validates with lucene', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - language: 'lucene', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - language: 'lucene', - }; - expect(message.schema).toEqual(expected); - }); - - test('language does not validate with something made up', () => { - const payload: Omit & { language: string } = { - ...getUpdateRulesSchemaMock(), - language: 'something-made-up', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "something-made-up" supplied to "language"', - ]); - expect(message.schema).toEqual({}); - }); - - test('max_signals cannot be negative', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - max_signals: -1, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "max_signals"', - ]); - expect(message.schema).toEqual({}); - }); - - test('max_signals cannot be zero', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - max_signals: 0, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to "max_signals"']); - expect(message.schema).toEqual({}); - }); - - test('max_signals can be 1', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - max_signals: 1, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - max_signals: 1, - }; - expect(message.schema).toEqual(expected); - }); - - test('You can optionally send in an array of tags', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - tags: ['tag_1', 'tag_2'], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - tags: ['tag_1', 'tag_2'], - }; - expect(message.schema).toEqual(expected); - }); - - test('You cannot send in an array of tags that are numbers', () => { - const payload: Omit & { tags: number[] } = { - ...getUpdateRulesSchemaMock(), - tags: [0, 1, 2], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "tags"', - 'Invalid value "1" supplied to "tags"', - 'Invalid value "2" supplied to "tags"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of threat that are missing "framework"', () => { - const payload: Omit & { - threat: Array>>; - } = { - ...getUpdateRulesSchemaMock(), - threat: [ - { - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,framework"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of threat that are missing "tactic"', () => { - const payload: Omit & { - threat: Array>>; - } = { - ...getUpdateRulesSchemaMock(), - threat: [ - { - framework: 'fake', - technique: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,tactic"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of threat that are missing "technique"', () => { - const payload: Omit & { - threat: Array>>; - } = { - ...getUpdateRulesSchemaMock(), - threat: [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - }, - ], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "threat,technique"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You can optionally send in an array of false positives', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - false_positives: ['false_1', 'false_2'], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - false_positives: ['false_1', 'false_2'], - }; - expect(message.schema).toEqual(expected); - }); - - test('You cannot send in an array of false positives that are numbers', () => { - const payload: Omit & { false_positives: number[] } = { - ...getUpdateRulesSchemaMock(), - false_positives: [5, 4], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "false_positives"', - 'Invalid value "4" supplied to "false_positives"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot set the immutable to a number when trying to update a rule', () => { - const payload: Omit & { immutable: number } = { - ...getUpdateRulesSchemaMock(), - immutable: 5, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "immutable"']); - expect(message.schema).toEqual({}); - }); - - test('You cannot set the risk_score to 101', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - risk_score: 101, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "101" supplied to "risk_score"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot set the risk_score to -1', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - risk_score: -1, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to "risk_score"']); - expect(message.schema).toEqual({}); - }); - - test('You can set the risk_score to 0', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - risk_score: 0, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - risk_score: 0, - }; - expect(message.schema).toEqual(expected); - }); - - test('You can set the risk_score to 100', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - risk_score: 100, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - risk_score: 100, - }; - expect(message.schema).toEqual(expected); - }); - - test('You can set meta to any object you want', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - meta: { - somethingMadeUp: { somethingElse: true }, - }, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - meta: { - somethingMadeUp: { somethingElse: true }, - }, - }; - expect(message.schema).toEqual(expected); - }); - - test('You cannot update meta as a string', () => { - const payload: Omit & { meta: string } = { - ...getUpdateRulesSchemaMock(), - meta: 'should not work', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "should not work" supplied to "meta"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You can omit the query string when filters are present', () => { - const { query, ...noQuery } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesSchema = { - ...noQuery, - filters: [], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { query: expectedQuery, ...expectedNoQuery } = getUpdateRulesSchemaDecodedMock(); - const expected: UpdateRulesSchemaDecoded = { - ...expectedNoQuery, - filters: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('validates with timeline_id and timeline_title', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - timeline_id: 'timeline-id', - timeline_title: 'timeline-title', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - timeline_id: 'timeline-id', - timeline_title: 'timeline-title', - }; - expect(message.schema).toEqual(expected); - }); - - test('The default for "from" will be "now-6m"', () => { - const { from, ...noFrom } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesSchema = { - ...noFrom, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { from: expectedFrom, ...expectedNoFrom } = getUpdateRulesSchemaDecodedMock(); - const expected: UpdateRulesSchemaDecoded = { - ...expectedNoFrom, - from: 'now-6m', - }; - expect(message.schema).toEqual(expected); - }); - - test('The default for "to" will be "now"', () => { - const { to, ...noTo } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesSchema = { - ...noTo, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { to: expectedTo, ...expectedNoTo } = getUpdateRulesSchemaDecodedMock(); - const expected: UpdateRulesSchemaDecoded = { - ...expectedNoTo, - to: 'now', - }; - expect(message.schema).toEqual(expected); - }); - - test('You cannot set the severity to a value other than low, medium, high, or critical', () => { - const payload: Omit & { severity: string } = { - ...getUpdateRulesSchemaMock(), - severity: 'junk', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "junk" supplied to "severity"']); - expect(message.schema).toEqual({}); - }); - - test('The default for "actions" will be an empty array', () => { - const { actions, ...noActions } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesSchema = { - ...noActions, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { actions: expectedActions, ...expectedNoActions } = getUpdateRulesSchemaDecodedMock(); - const expected: UpdateRulesSchemaDecoded = { - ...expectedNoActions, - actions: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('You cannot send in an array of actions that are missing "group"', () => { - const payload: Omit = { - ...getUpdateRulesSchemaMock(), - actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "actions,group"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of actions that are missing "id"', () => { - const payload: Omit = { - ...getUpdateRulesSchemaMock(), - actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "actions,id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of actions that are missing "action_type_id"', () => { - const payload: Omit = { - ...getUpdateRulesSchemaMock(), - actions: [{ group: 'group', id: 'id', params: {} }], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "actions,action_type_id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of actions that are missing "params"', () => { - const payload: Omit = { - ...getUpdateRulesSchemaMock(), - actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "actions,params"', - ]); - expect(message.schema).toEqual({}); - }); - - test('You cannot send in an array of actions that are including "actionTypeId"', () => { - const payload: Omit = { - ...getUpdateRulesSchemaMock(), - actions: [ - { - group: 'group', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "actions,action_type_id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('The default for "throttle" will be null', () => { - const { throttle, ...noThrottle } = getUpdateRulesSchemaMock(); - const payload: UpdateRulesSchema = { - ...noThrottle, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - - const { throttle: expectedThrottle, ...expectedNoThrottle } = getUpdateRulesSchemaDecodedMock(); - const expected: UpdateRulesSchemaDecoded = { - ...expectedNoThrottle, - throttle: null, - }; - expect(message.schema).toEqual(expected); - }); - - describe('note', () => { - test('You can set note to a string', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - note: '# documentation markdown here', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - note: '# documentation markdown here', - }; - expect(message.schema).toEqual(expected); - }); - - test('You can set note to an empty string', () => { - const payload: UpdateRulesSchema = { - ...getUpdateRulesSchemaMock(), - note: '', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - ...getUpdateRulesSchemaDecodedMock(), - note: '', - }; - expect(message.schema).toEqual(expected); - }); - - // Note: If you're looking to remove `note`, omit `note` entirely - test('You cannot set note to null', () => { - const payload: Omit & { note: null } = { - ...getUpdateRulesSchemaMock(), - note: null, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "null" supplied to "note"']); - expect(message.schema).toEqual({}); - }); - - test('You cannot set note as an object', () => { - const payload: Omit & { note: {} } = { - ...getUpdateRulesSchemaMock(), - note: { - somethingHere: 'something else', - }, - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "{"somethingHere":"something else"}" supplied to "note"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - }); - - describe('exception_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and exceptions_list] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - filters: [], - note: '# some markdown', - exceptions_list: getListArrayMock(), - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - filters: [], - exceptions_list: [ - { - id: 'some_uuid', - list_id: 'list_id_single', - namespace_type: 'single', - type: 'detection', - }, - { - id: 'endpoint_list', - list_id: 'endpoint_list', - namespace_type: 'agnostic', - type: 'endpoint', - }, - ], - }; - expect(message.schema).toEqual(expected); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - filters: [], - note: '# some markdown', - exceptions_list: [], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - filters: [], - exceptions_list: [], - }; - expect(message.schema).toEqual(expected); - }); - - test('rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and invalid exceptions_list] does NOT validate', () => { - const payload: Omit & { - exceptions_list: Array<{ id: string; namespace_type: string }>; - } = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - filters: [], - note: '# some markdown', - exceptions_list: [{ id: 'uuid_here', namespace_type: 'not a namespace type' }], - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "exceptions_list,list_id"', - 'Invalid value "undefined" supplied to "exceptions_list,type"', - 'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { - const payload: UpdateRulesSchema = { - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - filters: [], - note: '# some markdown', - }; - - const decoded = updateRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - const expected: UpdateRulesSchemaDecoded = { - author: [], - severity_mapping: [], - risk_score_mapping: [], - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'low', - interval: '5m', - type: 'query', - risk_score: 50, - note: '# some markdown', - references: [], - actions: [], - enabled: true, - false_positives: [], - max_signals: DEFAULT_MAX_SIGNALS, - tags: [], - threat: [], - throttle: null, - exceptions_list: [], - filters: [], - }; - expect(message.schema).toEqual(expected); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts deleted file mode 100644 index 5d759fc12cd528..00000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as t from 'io-ts'; - -import { - description, - anomaly_threshold, - filters, - RuleId, - index, - output_index, - saved_id, - timeline_id, - timeline_title, - meta, - machine_learning_job_id, - risk_score, - rule_id, - MaxSignals, - name, - severity, - Tags, - To, - type, - Threat, - threshold, - ThrottleOrNull, - note, - version, - References, - Actions, - Enabled, - FalsePositives, - From, - Interval, - language, - query, - id, - building_block_type, - license, - rule_name_override, - timestamp_override, - Author, - RiskScoreMapping, - SeverityMapping, - event_category_override, -} from '../common/schemas'; -import { - threat_index, - concurrent_searches, - items_per_search, - threat_query, - threat_filters, - threat_mapping, - threat_language, -} from '../types/threat_mapping'; - -import { - DefaultStringArray, - DefaultActionsArray, - DefaultBooleanTrue, - DefaultFromString, - DefaultIntervalString, - DefaultMaxSignalsNumber, - DefaultToString, - DefaultThreatArray, - DefaultThrottleNull, - DefaultListArray, - ListArray, - DefaultRiskScoreMappingArray, - DefaultSeverityMappingArray, -} from '../types'; - -/** - * This almost identical to the create_rules_schema except for a few details. - * - The version will not be defaulted to a 1. If it is not given then its default will become the previous version auto-incremented - * This does break idempotency slightly as calls repeatedly without it will increment the number. If the version number is passed in - * this will update the rule's version number. - * - id is on here because you can pass in an id to update using it instead of rule_id. - */ -export const updateRulesSchema = t.intersection([ - t.exact( - t.type({ - description, - risk_score, - name, - severity, - type, - }) - ), - t.exact( - t.partial({ - id, // defaults to "undefined" if not set during decode - actions: DefaultActionsArray, // defaults to empty actions array if not set during decode - anomaly_threshold, // defaults to undefined if not set during decode - author: DefaultStringArray, // defaults to empty array of strings if not set during decode - building_block_type, // defaults to undefined if not set during decode - enabled: DefaultBooleanTrue, // defaults to true if not set during decode - event_category_override, - false_positives: DefaultStringArray, // defaults to empty string array if not set during decode - filters, // defaults to undefined if not set during decode - from: DefaultFromString, // defaults to "now-6m" if not set during decode - rule_id, // defaults to "undefined" if not set during decode - index, // defaults to undefined if not set during decode - interval: DefaultIntervalString, // defaults to "5m" if not set during decode - query, // defaults to undefined if not set during decode - language, // defaults to undefined if not set during decode - license, // defaults to "undefined" if not set during decode - // TODO: output_index: This should be removed eventually - output_index, // defaults to "undefined" if not set during decode - saved_id, // defaults to "undefined" if not set during decode - timeline_id, // defaults to "undefined" if not set during decode - timeline_title, // defaults to "undefined" if not set during decode - meta, // defaults to "undefined" if not set during decode - machine_learning_job_id, // defaults to "undefined" if not set during decode - max_signals: DefaultMaxSignalsNumber, // defaults to DEFAULT_MAX_SIGNALS (100) if not set during decode - risk_score_mapping: DefaultRiskScoreMappingArray, // defaults to empty risk score mapping array if not set during decode - rule_name_override, // defaults to "undefined" if not set during decode - severity_mapping: DefaultSeverityMappingArray, // defaults to empty actions array if not set during decode - tags: DefaultStringArray, // defaults to empty string array if not set during decode - to: DefaultToString, // defaults to "now" if not set during decode - threat: DefaultThreatArray, // defaults to empty array if not set during decode - threshold, // defaults to "undefined" if not set during decode - throttle: DefaultThrottleNull, // defaults to "null" if not set during decode - timestamp_override, // defaults to "undefined" if not set during decode - references: DefaultStringArray, // defaults to empty array of strings if not set during decode - note, // defaults to "undefined" if not set during decode - version, // defaults to "undefined" if not set during decode - exceptions_list: DefaultListArray, // defaults to empty array if not set during decode - threat_mapping, // defaults to "undefined" if not set during decode - threat_query, // defaults to "undefined" if not set during decode - threat_filters, // defaults to "undefined" if not set during decode - threat_index, // defaults to "undefined" if not set during decode - threat_language, // defaults "undefined" if not set during decode - concurrent_searches, // defaults to "undefined" if not set during decode - items_per_search, // defaults to "undefined" if not set during decode - }) - ), -]); - -export type UpdateRulesSchema = t.TypeOf; - -// This type is used after a decode since some things are defaults after a decode. -export type UpdateRulesSchemaDecoded = Omit< - UpdateRulesSchema, - | 'author' - | 'references' - | 'actions' - | 'enabled' - | 'false_positives' - | 'from' - | 'interval' - | 'max_signals' - | 'risk_score_mapping' - | 'severity_mapping' - | 'tags' - | 'to' - | 'threat' - | 'throttle' - | 'exceptions_list' - | 'rule_id' -> & { - author: Author; - references: References; - actions: Actions; - enabled: Enabled; - false_positives: FalsePositives; - from: From; - interval: Interval; - max_signals: MaxSignals; - risk_score_mapping: RiskScoreMapping; - severity_mapping: SeverityMapping; - tags: Tags; - to: To; - threat: Threat; - throttle: ThrottleOrNull; - exceptions_list: ListArray; - rule_id: RuleId; -}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts index b3bbe6ef891f42..c246a1bff9f64a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getUpdateRulesSchemaMock } from './update_rules_schema.mock'; -import { UpdateRulesSchema } from './update_rules_schema'; +import { getUpdateRulesSchemaMock } from './rule_schemas.mock'; +import { UpdateRulesSchema } from './rule_schemas'; import { updateRuleValidateTypeDependents } from './update_rules_type_dependents'; describe('update_rules_type_dependents', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts index 6f50517cf2470b..e68ffd7925709c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UpdateRulesSchema } from './update_rules_schema'; +import { UpdateRulesSchema } from './rule_schemas'; export const validateTimelineId = (rule: UpdateRulesSchema): string[] => { if (rule.timeline_id != null) { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index a859af789d4a33..7bea035f921bc5 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -22,11 +22,13 @@ import { getPrePackagedRulesStatus, } from './api'; import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; -import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; +import { + getCreateRulesSchemaMock, + getUpdateRulesSchemaMock, +} from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock'; import { rulesMock } from './mock'; import { buildEsQuery } from 'src/plugins/data/common'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; jest.mock('../../../../common/lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx index 9603a4151933a4..95c5bef962a00a 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx @@ -7,7 +7,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useUpdateRule, ReturnUpdateRule } from './use_update_rule'; -import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; +import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('./api'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index eeb29d99f599cf..e1ef22c4531853 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -60,6 +60,7 @@ describe('patch_rules_bulk', () => { path: `${DETECTION_ENGINE_RULES_URL}/bulk_update`, body: [ { + type: 'machine_learning', rule_id: 'my-rule-id', anomaly_threshold: 4, machine_learning_job_id: 'some_job_id', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index f70afc8e59ee8d..72261f83cb6dc3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -19,7 +19,7 @@ import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { updateRulesRoute } from './update_rules_route'; -import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; +import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); jest.mock('../../rules/update_rules_notifications'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts index e37dbb47259bc2..4ecec264b1976d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fullUpdateSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; +import { updateRulesSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; import { updateRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/update_rules_type_dependents'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; @@ -24,7 +24,7 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { { path: DETECTION_ENGINE_RULES_URL, validate: { - body: buildRouteValidation(fullUpdateSchema), + body: buildRouteValidation(updateRulesSchema), }, options: { tags: ['access:securitySolution'], 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 41b98769111f19..13d03e4fe6caea 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 @@ -13,7 +13,7 @@ import { SavedObjectsFindResponse, SavedObjectsClientContract, } from 'kibana/server'; -import { FullUpdateSchema } from '../../../../common/detection_engine/schemas/request/rule_schemas'; +import { UpdateRulesSchema } from '../../../../common/detection_engine/schemas/request/rule_schemas'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { FalsePositives, @@ -254,7 +254,7 @@ export interface UpdateRulesOptions { savedObjectsClient: SavedObjectsClientContract; alertsClient: AlertsClient; defaultOutputIndex: string; - ruleUpdate: FullUpdateSchema; + ruleUpdate: UpdateRulesSchema; } export interface PatchRulesOptions { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts index a40a3d972d1760..ab71110072bfd4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts @@ -8,7 +8,7 @@ import { UpdateRulesOptions } from './types'; import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; import { - getFullUpdateSchemaMock, + getUpdateRulesSchemaMock, getUpdateMachineLearningSchemaMock, } from '../../../../common/detection_engine/schemas/request/rule_schemas.mock'; @@ -16,7 +16,7 @@ export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({ alertsClient: alertsClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(), defaultOutputIndex: '.siem-signals-default', - ruleUpdate: getFullUpdateSchemaMock(), + ruleUpdate: getUpdateRulesSchemaMock(), }); export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({ diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index c08d3c1fdd5d6e..71c9ad853964fc 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -26,7 +26,7 @@ import { Status, SignalIds, } from '../../plugins/security_solution/common/detection_engine/schemas/common/schemas'; -import { UpdateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema'; +import { UpdateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { RulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema'; import { DETECTION_ENGINE_INDEX_URL, From 2ef41220d2c34f70f946049cd0af19efe791d91f Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Tue, 10 Nov 2020 20:00:27 -0500 Subject: [PATCH 14/19] Cleanup - remove references to specific files within request folder --- .../schemas/request/create_rules_bulk_schema.ts | 2 +- .../schemas/request/create_rules_type_dependents.test.ts | 2 +- .../schemas/request/update_rules_type_dependents.ts | 2 +- .../detections/containers/detection_engine/rules/api.ts | 2 +- .../detections/containers/detection_engine/rules/types.ts | 2 +- .../containers/detection_engine/rules/use_create_rule.tsx | 2 +- .../pages/detection_engine/rules/create/helpers.test.ts | 2 +- .../detections/pages/detection_engine/rules/create/index.tsx | 2 +- .../detection_engine/routes/__mocks__/request_responses.ts | 2 +- .../lib/detection_engine/routes/rules/create_rules_route.ts | 2 +- .../lib/detection_engine/routes/rules/update_rules_route.ts | 2 +- .../server/lib/detection_engine/routes/rules/validate.ts | 2 +- .../server/lib/detection_engine/rules/types.ts | 2 +- .../server/lib/detection_engine/schemas/rule_converters.ts | 2 +- .../basic/tests/create_rules.ts | 2 +- .../security_and_spaces/tests/add_actions.ts | 2 +- .../security_and_spaces/tests/create_exceptions.ts | 2 +- .../security_and_spaces/tests/create_rules.ts | 2 +- .../security_and_spaces/tests/create_threat_matching.ts | 2 +- .../security_and_spaces/tests/generating_signals.ts | 2 +- x-pack/test/detection_engine_api_integration/utils.ts | 4 ++-- 21 files changed, 22 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts index 81b8bf7abbf786..a4e000ac791695 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts @@ -6,7 +6,7 @@ import * as t from 'io-ts'; -import { createRulesSchema } from './rule_schemas'; +import { createRulesSchema } from './'; export const createRulesBulkSchema = t.array(createRulesSchema); export type CreateRulesBulkSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts index 3c395df03e0f14..3e126a2e3e4c4a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts @@ -5,7 +5,7 @@ */ import { getCreateRulesSchemaMock, getCreateThreatMatchRulesSchemaMock } from './rule_schemas.mock'; -import { CreateRulesSchema } from './rule_schemas'; +import { CreateRulesSchema } from './'; import { createRuleValidateTypeDependents } from './create_rules_type_dependents'; describe('create_rules_type_dependents', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts index e68ffd7925709c..1cd4cbdb9af5aa 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UpdateRulesSchema } from './rule_schemas'; +import { UpdateRulesSchema } from './'; export const validateTimelineId = (rule: UpdateRulesSchema): string[] => { if (rule.timeline_id != null) { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 56e0165a454ae6..9512ae6f2d6e00 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { FullResponseSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; +import { FullResponseSchema } from '../../../../../common/detection_engine/schemas/request'; import { HttpStart } from '../../../../../../../../src/core/public'; import { DETECTION_ENGINE_RULES_URL, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index 2d68922a048c02..e9c89130736c05 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -6,7 +6,6 @@ import * as t from 'io-ts'; -import { CreateRulesSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; import { SortOrder, author, @@ -28,6 +27,7 @@ import { threat_filters, } from '../../../../../common/detection_engine/schemas/types'; import { + CreateRulesSchema, PatchRulesSchema, UpdateRulesSchema, } from '../../../../../common/detection_engine/schemas/request'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx index f8ae9e5cc72225..2bbd27994fc771 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx @@ -6,8 +6,8 @@ import { useEffect, useState, Dispatch } from 'react'; -import { CreateRulesSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; +import { CreateRulesSchema } from '../../../../../common/detection_engine/schemas/request'; import { createRule } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts index b37fd40e4418d3..239d885bfc1577 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts @@ -5,7 +5,7 @@ */ import { List } from '../../../../../../common/detection_engine/schemas/types'; -import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request/rule_schemas'; +import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; import { Rule } from '../../../../containers/detection_engine/rules'; import { getListMock, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index e18097a8e29aca..9d54879ee74953 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -16,8 +16,8 @@ import React, { useCallback, useRef, useState, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import styled, { StyledComponent } from 'styled-components'; -import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request/rule_schemas'; import { useCreateRule } from '../../../../containers/detection_engine/rules'; +import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; import { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index f8ba2a9f2d9179..fd29be0e81f3c6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -26,9 +26,9 @@ import { requestMock } from './request'; import { RuleNotificationAlertType } from '../../notifications/types'; import { QuerySignalsSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema'; import { SetSignalsStatusSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { EqlSearchResponse } from '../../../../../common/detection_engine/types'; -import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ signal_ids: ['somefakeid1', 'somefakeid2'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index 98f567e917bbca..2493da99eeab35 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -26,7 +26,7 @@ import { updateRulesNotifications } from '../../rules/update_rules_notifications import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { addTags } from '../../rules/add_tags'; import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions'; -import { createRulesSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; +import { createRulesSchema } from '../../../../../common/detection_engine/schemas/request'; import { newTransformValidate } from './validate'; import { InternalRuleCreate } from '../../schemas/rule_schemas'; import { typeSpecificSnakeToCamel } from '../../schemas/rule_converters'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts index 4ecec264b1976d..aa85747e3ce41e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { updateRulesSchema } from '../../../../../common/detection_engine/schemas/request/rule_schemas'; +import { updateRulesSchema } from '../../../../../common/detection_engine/schemas/request'; import { updateRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/update_rules_type_dependents'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts index bf152e39360d09..382186df16cd18 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts @@ -12,7 +12,7 @@ import * as t from 'io-ts'; import { FullResponseSchema, fullResponseSchema, -} from '../../../../../common/detection_engine/schemas/request/rule_schemas'; +} from '../../../../../common/detection_engine/schemas/request'; import { validate } from '../../../../../common/validate'; import { findRulesSchema } from '../../../../../common/detection_engine/schemas/response/find_rules_schema'; import { 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 13d03e4fe6caea..45186f9978650c 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 @@ -13,7 +13,7 @@ import { SavedObjectsFindResponse, SavedObjectsClientContract, } from 'kibana/server'; -import { UpdateRulesSchema } from '../../../../common/detection_engine/schemas/request/rule_schemas'; +import { UpdateRulesSchema } from '../../../../common/detection_engine/schemas/request'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { FalsePositives, 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 0f76cea85bcbae..5c8ffc6bdde5a8 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 @@ -10,7 +10,7 @@ import { CreateTypeSpecific, FullResponseSchema, ResponseTypeSpecific, -} from '../../../../common/detection_engine/schemas/request/rule_schemas'; +} from '../../../../common/detection_engine/schemas/request'; import { RuleActions } from '../rule_actions/types'; // These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts index 22e3849b686ca1..53a8f1f4ca5c0b 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts index a98b14c3331b81..d473863e7d028b 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts index 31b1858573586d..651a7601ca95a8 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts @@ -7,7 +7,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import expect from '@kbn/expect'; -import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; import { deleteAllExceptions } from '../../../lists_api_integration/utils'; import { RulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/response'; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index d2c7ab6a7db6ff..a18faf8543042e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; import { DETECTION_ENGINE_RULES_URL, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index c6c1013b6f4e5f..36cd8480998c56 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_RULES_STATUS_URL, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 778af8168f08e7..0ba2abb466f7bb 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { QueryCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; +import { QueryCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; import { DEFAULT_SIGNALS_INDEX } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 71c9ad853964fc..f458fe118dcf7d 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -11,9 +11,10 @@ import { Context } from '@elastic/elasticsearch/lib/Transport'; import { SearchResponse } from 'elasticsearch'; import { CreateRulesSchema, + UpdateRulesSchema, FullResponseSchema, QueryCreateSchema, -} from '../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; +} from '../../plugins/security_solution/common/detection_engine/schemas/request'; import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../../plugins/lists/common/constants'; import { CreateExceptionListItemSchema, @@ -26,7 +27,6 @@ import { Status, SignalIds, } from '../../plugins/security_solution/common/detection_engine/schemas/common/schemas'; -import { UpdateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas'; import { RulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema'; import { DETECTION_ENGINE_INDEX_URL, From 6bf357208075e6f1c1393377ab7d932ed72cb109 Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Tue, 10 Nov 2020 21:30:13 -0500 Subject: [PATCH 15/19] Fix imports --- .../schemas/request/create_rules_bulk_schema.ts | 2 +- .../schemas/request/create_rules_type_dependents.test.ts | 2 +- .../schemas/request/update_rules_type_dependents.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts index a4e000ac791695..81b8bf7abbf786 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.ts @@ -6,7 +6,7 @@ import * as t from 'io-ts'; -import { createRulesSchema } from './'; +import { createRulesSchema } from './rule_schemas'; export const createRulesBulkSchema = t.array(createRulesSchema); export type CreateRulesBulkSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts index 3e126a2e3e4c4a..3c395df03e0f14 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts @@ -5,7 +5,7 @@ */ import { getCreateRulesSchemaMock, getCreateThreatMatchRulesSchemaMock } from './rule_schemas.mock'; -import { CreateRulesSchema } from './'; +import { CreateRulesSchema } from './rule_schemas'; import { createRuleValidateTypeDependents } from './create_rules_type_dependents'; describe('create_rules_type_dependents', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts index 1cd4cbdb9af5aa..e68ffd7925709c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_type_dependents.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UpdateRulesSchema } from './'; +import { UpdateRulesSchema } from './rule_schemas'; export const validateTimelineId = (rule: UpdateRulesSchema): string[] => { if (rule.timeline_id != null) { From e7cbd53924e97aa58096650b5064d87a475004da Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Tue, 10 Nov 2020 22:38:13 -0500 Subject: [PATCH 16/19] Fix tests --- .../detections/containers/detection_engine/rules/api.test.ts | 2 +- .../detection_engine/routes/rules/update_rules_route.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index 7bea035f921bc5..e94cc8845c5a58 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -66,7 +66,7 @@ describe('Detections Rules API', () => { await updateRule({ rule: payload, signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { body: - '{"description":"some description","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","rule_id":"rule-1"}', + '{"description":"Detecting root and admin users","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd"}', method: 'PUT', signal: abortCtrl.signal, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index 72261f83cb6dc3..96710b6f1d7637 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -131,7 +131,7 @@ describe('update_rules', () => { path: DETECTION_ENGINE_RULES_URL, body: { ...getUpdateRulesSchemaMock(), - rule_id: undefined, + id: undefined, }, }); const response = await server.inject(noIdRequest, context); From 9143785aaa4b09af65c7b851362e3c4bb729ba17 Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Thu, 12 Nov 2020 01:39:57 -0500 Subject: [PATCH 17/19] Allow a few more fields to be undefined in internal schema --- .../lib/detection_engine/rules/patch_rules.ts | 2 +- .../detection_engine/schemas/rule_converters.ts | 8 ++++---- .../lib/detection_engine/schemas/rule_schemas.ts | 16 ++++++++-------- 3 files changed, 13 insertions(+), 13 deletions(-) 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 b50499af3a6f56..8e10fc21f040c2 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 @@ -181,7 +181,7 @@ export const patchRules = async ({ }; const [validated, errors] = validate(newRule, internalRuleUpdate); if (errors != null || validated === null) { - throw new PatchError('Applying patch would create invalid rule', 400); + throw new PatchError(`Applying patch would create invalid rule: ${errors}`, 400); } const update = await alertsClient.update({ 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 5c8ffc6bdde5a8..e16543c99bbe4c 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 @@ -197,18 +197,18 @@ export const internalRuleToAPIResponse = ( meta: rule.params.meta, rule_name_override: rule.params.ruleNameOverride, timestamp_override: rule.params.timestampOverride, - author: rule.params.author, + author: rule.params.author ?? [], false_positives: rule.params.falsePositives, from: rule.params.from, rule_id: rule.params.ruleId, max_signals: rule.params.maxSignals, - risk_score_mapping: rule.params.riskScoreMapping, - severity_mapping: rule.params.severityMapping, + risk_score_mapping: rule.params.riskScoreMapping ?? [], + severity_mapping: rule.params.severityMapping ?? [], threat: rule.params.threat, to: rule.params.to, references: rule.params.references, version: rule.params.version, - exceptions_list: rule.params.exceptionsList, + exceptions_list: rule.params.exceptionsList ?? [], ...typeSpecificCamelToSnake(rule.params), }; }; 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 fdf46c1e1d012f..5bb8d6d6746f9b 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 @@ -6,7 +6,7 @@ import * as t from 'io-ts'; -import { listArray } from '../../../../common/detection_engine/schemas/types/lists'; +import { listArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists'; import { threat_mapping, threat_index, @@ -15,7 +15,7 @@ import { itemsPerSearchOrUndefined, } from '../../../../common/detection_engine/schemas/types/threat_mapping'; import { - author, + authorOrUndefined, buildingBlockTypeOrUndefined, description, enabled, @@ -37,10 +37,10 @@ import { machine_learning_job_id, max_signals, risk_score, - risk_score_mapping, + riskScoreMappingOrUndefined, ruleNameOverrideOrUndefined, severity, - severity_mapping, + severityMappingOrUndefined, tags, timestampOverrideOrUndefined, threat, @@ -64,7 +64,7 @@ import { SIGNALS_ID, SERVER_APP_ID } from '../../../../common/constants'; const nonEqlLanguages = t.keyof({ kuery: null, lucene: null }); export const baseRuleParams = t.exact( t.type({ - author, + author: authorOrUndefined, buildingBlockType: buildingBlockTypeOrUndefined, description, note: noteOrUndefined, @@ -80,16 +80,16 @@ export const baseRuleParams = t.exact( // maxSignals not used in ML rules but probably should be used maxSignals: max_signals, riskScore: risk_score, - riskScoreMapping: risk_score_mapping, + riskScoreMapping: riskScoreMappingOrUndefined, ruleNameOverride: ruleNameOverrideOrUndefined, severity, - severityMapping: severity_mapping, + severityMapping: severityMappingOrUndefined, timestampOverride: timestampOverrideOrUndefined, threat, to, references, version, - exceptionsList: listArray, + exceptionsList: listArrayOrUndefined, }) ); export type BaseRuleParams = t.TypeOf; From beccc221b7bff82d9d4944c76bb2de14586d33f1 Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Thu, 12 Nov 2020 10:47:06 -0500 Subject: [PATCH 18/19] Add static types back to test payloads, add more tests, add NonEmptyArray type builder --- .../schemas/request/rule_schemas.mock.ts | 15 ++ .../schemas/request/rule_schemas.test.ts | 135 +++++++++++++----- .../detection_engine/schemas/types/index.ts | 1 + .../schemas/types/non_empty_array.test.ts | 94 ++++++++++++ .../schemas/types/non_empty_array.ts | 28 ++++ .../schemas/types/threat_mapping.test.ts | 13 ++ .../schemas/types/threat_mapping.ts | 3 +- 7 files changed, 254 insertions(+), 35 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.test.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts index edcf1173c26e25..6be51d2a1adc23 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts @@ -13,6 +13,7 @@ import { SavedQueryUpdateSchema, ThreatMatchCreateSchema, ThreatMatchUpdateSchema, + ThresholdCreateSchema, } from './rule_schemas'; export const getCreateRulesSchemaMock = (ruleId = 'rule-1'): QueryCreateSchema => ({ @@ -95,6 +96,20 @@ export const getCreateMachineLearningRulesSchemaMock = ( machine_learning_job_id: 'typical-ml-job-id', }); +export const getCreateThresholdRulesSchemaMock = (ruleId = 'rule-1'): ThresholdCreateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + risk_score: 55, + rule_id: ruleId, + type: 'threshold', + query: 'user.name: root or user.name: admin', + threshold: { + field: 'some.field', + value: 4, + }, +}); + export const getUpdateRulesSchemaMock = ( id = '04128c15-0d1b-4716-a4c5-46997ac7f3bd' ): QueryUpdateSchema => ({ diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts index 51b984875c921c..c9330bbe73c89d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts @@ -13,6 +13,7 @@ import { getCreateSavedQueryRulesSchemaMock, getCreateThreatMatchRulesSchemaMock, getCreateRulesSchemaMock, + getCreateThresholdRulesSchemaMock, } from './rule_schemas.mock'; import { getListArrayMock } from '../types/lists.mock'; @@ -41,7 +42,7 @@ describe('create rules schema', () => { }); test('[rule_id] does not validate', () => { - const payload = { + const payload: Partial = { rule_id: 'rule-1', }; @@ -53,7 +54,7 @@ describe('create rules schema', () => { }); test('[rule_id, description] does not validate', () => { - const payload = { + const payload: Partial = { rule_id: 'rule-1', description: 'some description', }; @@ -66,7 +67,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from] does not validate', () => { - const payload = { + const payload: Partial = { rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -80,7 +81,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to] does not validate', () => { - const payload = { + const payload: Partial = { rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -95,7 +96,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to, name] does not validate', () => { - const payload = { + const payload: Partial = { rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -111,7 +112,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to, name, severity] does not validate', () => { - const payload = { + const payload: Partial = { rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -128,7 +129,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to, name, severity, type] does not validate', () => { - const payload = { + const payload: Partial = { rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -148,7 +149,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { - const payload = { + const payload: Partial = { rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -169,7 +170,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { - const payload = { + const payload: Partial = { rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -191,7 +192,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { - const payload = { + const payload: CreateRulesSchema = { rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -213,7 +214,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { - const payload = { + const payload: Partial = { rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -449,7 +450,7 @@ describe('create rules schema', () => { }); test('language validates with kuery', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), language: 'kuery', }; @@ -462,7 +463,7 @@ describe('create rules schema', () => { }); test('language validates with lucene', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), language: 'lucene', }; @@ -490,7 +491,7 @@ describe('create rules schema', () => { }); test('max_signals cannot be negative', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), max_signals: -1, }; @@ -505,7 +506,7 @@ describe('create rules schema', () => { }); test('max_signals cannot be zero', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), max_signals: 0, }; @@ -518,7 +519,7 @@ describe('create rules schema', () => { }); test('max_signals can be 1', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), max_signals: 1, }; @@ -531,7 +532,7 @@ describe('create rules schema', () => { }); test('You can optionally send in an array of tags', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), tags: ['tag_1', 'tag_2'], }; @@ -641,7 +642,7 @@ describe('create rules schema', () => { }); test('You can optionally send in an array of false positives', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), false_positives: ['false_1', 'false_2'], }; @@ -683,7 +684,7 @@ describe('create rules schema', () => { }); test('You cannot set the risk_score to 101', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), risk_score: 101, }; @@ -698,7 +699,7 @@ describe('create rules schema', () => { }); test('You cannot set the risk_score to -1', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), risk_score: -1, }; @@ -711,7 +712,7 @@ describe('create rules schema', () => { }); test('You can set the risk_score to 0', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), risk_score: 0, }; @@ -724,7 +725,7 @@ describe('create rules schema', () => { }); test('You can set the risk_score to 100', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), risk_score: 100, }; @@ -737,7 +738,7 @@ describe('create rules schema', () => { }); test('You can set meta to any object you want', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), meta: { somethingMadeUp: { somethingElse: true }, @@ -768,7 +769,7 @@ describe('create rules schema', () => { test('You can omit the query string when filters are present', () => { const { query, ...noQuery } = getCreateRulesSchemaMock(); - const payload = { + const payload: CreateRulesSchema = { ...noQuery, filters: [], }; @@ -781,7 +782,7 @@ describe('create rules schema', () => { }); test('validates with timeline_id and timeline_title', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), timeline_id: 'timeline-id', timeline_title: 'timeline-title', @@ -891,7 +892,7 @@ describe('create rules schema', () => { describe('note', () => { test('You can set note to a string', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), note: '# documentation markdown here', }; @@ -904,7 +905,7 @@ describe('create rules schema', () => { }); test('You can set note to an empty string', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), note: '', }; @@ -934,7 +935,7 @@ describe('create rules schema', () => { }); test('empty name is not valid', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), name: '', }; @@ -947,7 +948,7 @@ describe('create rules schema', () => { }); test('empty description is not valid', () => { - const payload = { + const payload: CreateRulesSchema = { ...getCreateRulesSchemaMock(), description: '', }; @@ -962,7 +963,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note] does validate', () => { - const payload = { + const payload: CreateRulesSchema = { rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -985,7 +986,7 @@ describe('create rules schema', () => { }); test('machine_learning type does validate', () => { - const payload = { + const payload: CreateRulesSchema = { type: 'machine_learning', anomaly_threshold: 50, machine_learning_job_id: 'linux_anomalous_network_activity_ecs', @@ -1014,9 +1015,44 @@ describe('create rules schema', () => { expect(message.schema).toEqual(payload); }); + test('saved_id is required when type is saved_query and will not validate without it', () => { + /* eslint-disable @typescript-eslint/naming-convention */ + const { saved_id, ...payload } = getCreateSavedQueryRulesSchemaMock(); + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "saved_id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('threshold is required when type is threshold and will not validate without it', () => { + const { threshold, ...payload } = getCreateThresholdRulesSchemaMock(); + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "threshold"', + ]); + expect(message.schema).toEqual({}); + }); + + test('threshold rules fail validation if threshold is not greater than 0', () => { + const payload = getCreateThresholdRulesSchemaMock(); + payload.threshold.value = 0; + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "threshold,value"', + ]); + expect(message.schema).toEqual({}); + }); + describe('exception_list', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and exceptions_list] does validate', () => { - const payload = { + const payload: CreateRulesSchema = { rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1040,7 +1076,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { - const payload = { + const payload: CreateRulesSchema = { rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1092,7 +1128,7 @@ describe('create rules schema', () => { }); test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { - const payload = { + const payload: CreateRulesSchema = { rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1124,5 +1160,36 @@ describe('create rules schema', () => { expect(getPaths(left(message.errors))).toEqual([]); expect(message.schema).toEqual(payload); }); + + test('threat_index, threat_query, and threat_mapping are required when type is "threat_match" and validation fails without them', () => { + /* eslint-disable @typescript-eslint/naming-convention */ + const { + threat_index, + threat_query, + threat_mapping, + ...payload + } = getCreateThreatMatchRulesSchemaMock(); + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "threat_query"', + 'Invalid value "undefined" supplied to "threat_mapping"', + 'Invalid value "undefined" supplied to "threat_index"', + ]); + expect(message.schema).toEqual({}); + }); + + test('fails validation when threat_mapping is an empty array', () => { + const payload = getCreateThreatMatchRulesSchemaMock(); + payload.threat_mapping = []; + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "threat_mapping"', + ]); + expect(message.schema).toEqual({}); + }); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts index e76dd3fca37403..de0625e6b58177 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts @@ -28,6 +28,7 @@ export * from './default_version_number'; export * from './iso_date_string'; export * from './lists'; export * from './lists_default_array'; +export * from './non_empty_array'; export * from './non_empty_string'; export * from './only_false_allowed'; export * from './positive_integer'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.test.ts new file mode 100644 index 00000000000000..299cc924079835 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +import { NonEmptyArray } from './non_empty_array'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../test_utils'; + +const testSchema = t.keyof({ + valid: true, + also_valid: true, +}); +type TestSchema = t.TypeOf; + +const nonEmptyArraySchema = NonEmptyArray(testSchema, 'TestSchemaArray'); + +describe('non empty array', () => { + test('it should generate the correct name for non empty array', () => { + const newTestSchema = NonEmptyArray(testSchema); + expect(newTestSchema.name).toEqual('NonEmptyArray<"valid" | "also_valid">'); + }); + + test('it should use a supplied name override', () => { + const newTestSchema = NonEmptyArray(testSchema, 'someName'); + expect(newTestSchema.name).toEqual('someName'); + }); + + test('it should NOT validate an empty array', () => { + const payload: string[] = []; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate an array of testSchema', () => { + const payload: TestSchema[] = ['valid']; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of valid testSchema strings', () => { + const payload: TestSchema[] = ['valid', 'also_valid']; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = ['valid', 123]; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "123" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an array with an invalid string', () => { + const payload = ['valid', 'invalid']; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a null value', () => { + const payload = null; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "null" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts new file mode 100644 index 00000000000000..433ee5a4bf0803 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +export const NonEmptyArray = ( + codec: C, + name: string = `NonEmptyArray<${codec.name}>` +) => { + const arrType = t.array(codec); + type ArrType = t.TypeOf; + return new t.Type( + name, + arrType.is, + (input, context): Either => { + if (Array.isArray(input) && input.length === 0) { + return t.failure(input, context); + } else { + return arrType.validate(input, context); + } + }, + t.identity + ); +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts index d8f61e4309b17a..d3e8b95e69c3a3 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts @@ -176,6 +176,19 @@ describe('threat_mapping', () => { expect(message.schema).toEqual({}); }); + test('it should fail validate with empty array', () => { + const payload: string[] = []; + + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "NonEmptyArray"', + ]); + expect(message.schema).toEqual({}); + }); + test('it should fail validation when concurrent_searches is < 0', () => { const payload = -1; const decoded = concurrent_searches.decode(payload); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts index dec8ddd0001324..ad54d05863ad3d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts @@ -8,6 +8,7 @@ import * as t from 'io-ts'; import { language } from '../common/schemas'; +import { NonEmptyArray } from './non_empty_array'; import { NonEmptyString } from './non_empty_string'; import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero'; @@ -41,7 +42,7 @@ export const threatMap = t.exact( ); export type ThreatMap = t.TypeOf; -export const threat_mapping = t.array(threatMap); +export const threat_mapping = NonEmptyArray(threatMap, 'NonEmptyArray'); export type ThreatMapping = t.TypeOf; export const threatMappingOrUndefined = t.union([threat_mapping, t.undefined]); From 9aac141d57a409be10eb66d97d383a7f1b30749c Mon Sep 17 00:00:00 2001 From: Marshall Main Date: Thu, 12 Nov 2020 12:35:53 -0500 Subject: [PATCH 19/19] Pull defaults into reusable function --- .../routes/rules/create_rules_bulk_route.ts | 70 +++---------------- .../routes/rules/create_rules_route.ts | 61 ++-------------- .../schemas/rule_converters.ts | 54 +++++++++++++- 3 files changed, 67 insertions(+), 118 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 8687f56936efa7..b185b8780abe29 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -4,21 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable complexity */ - -import uuid from 'uuid'; -import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions'; import { validate } from '../../../../../common/validate'; import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents'; import { createRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; import { IRouter } from '../../../../../../../../src/core/server'; -import { - DEFAULT_MAX_SIGNALS, - DETECTION_ENGINE_RULES_URL, - SERVER_APP_ID, - SIGNALS_ID, -} from '../../../../../common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; @@ -30,9 +21,7 @@ import { buildRouteValidation } from '../../../../utils/build_validation/route_v import { transformBulkError, createBulkErrorObject, buildSiemResponse } from '../utils'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; -import { typeSpecificSnakeToCamel } from '../../schemas/rule_converters'; -import { InternalRuleCreate } from '../../schemas/rule_schemas'; -import { addTags } from '../../rules/add_tags'; +import { convertCreateAPIToInternalSchema } from '../../schemas/rule_converters'; export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => { router.post( @@ -84,50 +73,7 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => }); } } - const typeSpecificParams = typeSpecificSnakeToCamel(payloadRule); - const newRuleId = payloadRule.rule_id ?? uuid.v4(); - const throttle = payloadRule.throttle ?? null; - const internalRule: InternalRuleCreate = { - name: payloadRule.name, - tags: addTags(payloadRule.tags ?? [], newRuleId, false), - alertTypeId: SIGNALS_ID, - consumer: SERVER_APP_ID, - params: { - author: payloadRule.author ?? [], - buildingBlockType: payloadRule.building_block_type, - description: payloadRule.description, - ruleId: newRuleId, - falsePositives: payloadRule.false_positives ?? [], - from: payloadRule.from ?? 'now-6m', - immutable: false, - license: payloadRule.license, - outputIndex: payloadRule.output_index ?? siemClient.getSignalsIndex(), - timelineId: payloadRule.timeline_id, - timelineTitle: payloadRule.timeline_title, - meta: payloadRule.meta, - maxSignals: payloadRule.max_signals ?? DEFAULT_MAX_SIGNALS, - riskScore: payloadRule.risk_score, - riskScoreMapping: payloadRule.risk_score_mapping ?? [], - ruleNameOverride: payloadRule.rule_name_override, - severity: payloadRule.severity, - severityMapping: payloadRule.severity_mapping ?? [], - threat: payloadRule.threat ?? [], - timestampOverride: payloadRule.timestamp_override, - to: payloadRule.to ?? 'now', - references: payloadRule.references ?? [], - note: payloadRule.note, - version: payloadRule.version ?? 1, - exceptionsList: payloadRule.exceptions_list ?? [], - ...typeSpecificParams, - }, - schedule: { interval: payloadRule.interval ?? '5m' }, - enabled: payloadRule.enabled ?? true, - actions: - throttle === 'rule' - ? (payloadRule.actions ?? []).map(transformRuleToAlertAction) - : [], - throttle: null, - }; + const internalRule = convertCreateAPIToInternalSchema(payloadRule, siemClient); try { const validationErrors = createRuleValidateTypeDependents(payloadRule); if (validationErrors.length) { @@ -159,13 +105,17 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => savedObjectsClient, enabled: createdRule.enabled, actions: payloadRule.actions, - throttle, + throttle: payloadRule.throttle ?? null, name: createdRule.name, }); - return transformValidateBulkError(newRuleId, createdRule, ruleActions); + return transformValidateBulkError( + internalRule.params.ruleId, + createdRule, + ruleActions + ); } catch (err) { - return transformBulkError(newRuleId, err); + return transformBulkError(internalRule.params.ruleId, err); } }) ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index 2493da99eeab35..b52248f6701882 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -4,18 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable complexity */ - -import uuid from 'uuid'; - import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { IRouter } from '../../../../../../../../src/core/server'; -import { - DETECTION_ENGINE_RULES_URL, - SIGNALS_ID, - SERVER_APP_ID, - DEFAULT_MAX_SIGNALS, -} from '../../../../../common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; @@ -24,13 +15,10 @@ import { getIndexExists } from '../../index/get_index_exists'; import { transformError, buildSiemResponse } from '../utils'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; -import { addTags } from '../../rules/add_tags'; -import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions'; import { createRulesSchema } from '../../../../../common/detection_engine/schemas/request'; import { newTransformValidate } from './validate'; -import { InternalRuleCreate } from '../../schemas/rule_schemas'; -import { typeSpecificSnakeToCamel } from '../../schemas/rule_converters'; import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents'; +import { convertCreateAPIToInternalSchema } from '../../schemas/rule_converters'; export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void => { router.post( @@ -73,48 +61,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void } } - const typeSpecificParams = typeSpecificSnakeToCamel(request.body); - const newRuleId = request.body.rule_id ?? uuid.v4(); - const throttle = request.body.throttle ?? null; - const internalRule: InternalRuleCreate = { - name: request.body.name, - tags: addTags(request.body.tags ?? [], newRuleId, false), - alertTypeId: SIGNALS_ID, - consumer: SERVER_APP_ID, - params: { - author: request.body.author ?? [], - buildingBlockType: request.body.building_block_type, - description: request.body.description, - ruleId: newRuleId, - falsePositives: request.body.false_positives ?? [], - from: request.body.from ?? 'now-6m', - immutable: false, - license: request.body.license, - outputIndex: request.body.output_index ?? siemClient.getSignalsIndex(), - timelineId: request.body.timeline_id, - timelineTitle: request.body.timeline_title, - meta: request.body.meta, - maxSignals: request.body.max_signals ?? DEFAULT_MAX_SIGNALS, - riskScore: request.body.risk_score, - riskScoreMapping: request.body.risk_score_mapping ?? [], - ruleNameOverride: request.body.rule_name_override, - severity: request.body.severity, - severityMapping: request.body.severity_mapping ?? [], - threat: request.body.threat ?? [], - timestampOverride: request.body.timestamp_override, - to: request.body.to ?? 'now', - references: request.body.references ?? [], - note: request.body.note, - version: request.body.version ?? 1, - exceptionsList: request.body.exceptions_list ?? [], - ...typeSpecificParams, - }, - schedule: { interval: request.body.interval ?? '5m' }, - enabled: request.body.enabled ?? true, - actions: - throttle === 'rule' ? (request.body.actions ?? []).map(transformRuleToAlertAction) : [], - throttle: null, - }; + const internalRule = convertCreateAPIToInternalSchema(request.body, siemClient); const mlAuthz = buildMlAuthz({ license: context.licensing.license, @@ -148,7 +95,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void savedObjectsClient, enabled: createdRule.enabled, actions: request.body.actions, - throttle, + throttle: request.body.throttle ?? null, name: createdRule.name, }); 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 e16543c99bbe4c..86d85cd2a066e0 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 @@ -4,14 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InternalRuleResponse, TypeSpecificRuleParams } from './rule_schemas'; +import uuid from 'uuid'; +import { InternalRuleCreate, InternalRuleResponse, TypeSpecificRuleParams } from './rule_schemas'; import { assertUnreachable } from '../../../../common/utility_types'; import { + CreateRulesSchema, CreateTypeSpecific, FullResponseSchema, ResponseTypeSpecific, } from '../../../../common/detection_engine/schemas/request'; import { RuleActions } from '../rule_actions/types'; +import { AppClient } from '../../../types'; +import { addTags } from '../rules/add_tags'; +import { DEFAULT_MAX_SIGNALS, SERVER_APP_ID, SIGNALS_ID } from '../../../../common/constants'; +import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; // These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema // to the response API schema. This provides static type-check assurances that the internal schema is in sync with the API schema for @@ -94,6 +100,52 @@ export const typeSpecificSnakeToCamel = (params: CreateTypeSpecific): TypeSpecif } }; +export const convertCreateAPIToInternalSchema = ( + input: CreateRulesSchema, + siemClient: AppClient +): InternalRuleCreate => { + const typeSpecificParams = typeSpecificSnakeToCamel(input); + const newRuleId = input.rule_id ?? uuid.v4(); + return { + name: input.name, + tags: addTags(input.tags ?? [], newRuleId, false), + alertTypeId: SIGNALS_ID, + consumer: SERVER_APP_ID, + params: { + author: input.author ?? [], + buildingBlockType: input.building_block_type, + description: input.description, + ruleId: newRuleId, + falsePositives: input.false_positives ?? [], + from: input.from ?? 'now-6m', + immutable: false, + license: input.license, + outputIndex: input.output_index ?? siemClient.getSignalsIndex(), + timelineId: input.timeline_id, + timelineTitle: input.timeline_title, + meta: input.meta, + maxSignals: input.max_signals ?? DEFAULT_MAX_SIGNALS, + riskScore: input.risk_score, + riskScoreMapping: input.risk_score_mapping ?? [], + ruleNameOverride: input.rule_name_override, + severity: input.severity, + severityMapping: input.severity_mapping ?? [], + threat: input.threat ?? [], + timestampOverride: input.timestamp_override, + to: input.to ?? 'now', + references: input.references ?? [], + note: input.note, + version: input.version ?? 1, + exceptionsList: input.exceptions_list ?? [], + ...typeSpecificParams, + }, + schedule: { interval: input.interval ?? '5m' }, + enabled: input.enabled ?? true, + actions: input.throttle === 'rule' ? (input.actions ?? []).map(transformRuleToAlertAction) : [], + throttle: null, + }; +}; + // Converts the internal rule data structure to the response API schema export const typeSpecificCamelToSnake = (params: TypeSpecificRuleParams): ResponseTypeSpecific => { switch (params.type) {