From d5eb17528c52eac8b97583dec8c5ef93dc355fc5 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Wed, 26 Feb 2020 13:57:37 -0700 Subject: [PATCH] [SIEM][Detection Engine] Output schema validations (#58317) ## Summary * Adds an exact function which allows us to do exact validation rather than allowing arbitrary keys to go through * Adds a type dependency for parts of validation (gives us feature parity with joi.when()) * Adds several hand spun validations/codecs * Adds several utilities such as a printer for error validations ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../routes/__mocks__/request_responses.ts | 2 - .../rules/add_prepackaged_rules_route.ts | 18 +- .../routes/rules/create_rules_bulk_route.ts | 19 +- .../routes/rules/create_rules_route.test.ts | 2 +- .../routes/rules/create_rules_route.ts | 11 +- .../routes/rules/delete_rules_bulk_route.ts | 18 +- .../routes/rules/delete_rules_route.test.ts | 2 +- .../routes/rules/delete_rules_route.ts | 12 +- .../routes/rules/find_rules_route.test.ts | 2 +- .../routes/rules/find_rules_route.ts | 11 +- .../get_prepackaged_rules_status_route.ts | 18 +- .../routes/rules/import_rules_route.ts | 38 +- .../routes/rules/patch_rules_bulk_route.ts | 18 +- .../routes/rules/patch_rules_route.test.ts | 2 +- .../routes/rules/patch_rules_route.ts | 12 +- .../routes/rules/read_rules_route.test.ts | 2 +- .../routes/rules/read_rules_route.ts | 11 +- .../routes/rules/update_rules_bulk_route.ts | 19 +- .../routes/rules/update_rules_route.test.ts | 2 +- .../routes/rules/update_rules_route.ts | 12 +- .../routes/rules/utils.test.ts | 57 ++- .../detection_engine/routes/rules/utils.ts | 46 +- .../routes/rules/validate.test.ts | 155 +++++++ .../detection_engine/routes/rules/validate.ts | 119 +++++ .../schemas/response/__mocks__/utils.ts | 100 +++++ .../response/check_type_dependents.test.ts | 405 ++++++++++++++++++ .../schemas/response/check_type_dependents.ts | 70 +++ .../schemas/response/error_schema.test.ts | 59 +++ .../routes/schemas/response/error_schema.ts | 27 ++ .../schemas/response/exact_check.test.ts | 146 +++++++ .../routes/schemas/response/exact_check.ts | 84 ++++ .../response/find_rules_schema.test.ts | 128 ++++++ .../schemas/response/find_rules_schema.ts | 21 + .../response/import_rules_schema.test.ts | 120 ++++++ .../schemas/response/import_rules_schema.ts | 22 + .../response/prepackaged_rules_schema.test.ts | 87 ++++ .../response/prepackaged_rules_schema.ts | 20 + .../prepackaged_rules_status_schema.test.ts | 133 ++++++ .../prepackaged_rules_status_schema.ts | 27 ++ .../response/rules_bulk_schema.test.ts | 131 ++++++ .../schemas/response/rules_bulk_schema.ts | 13 + .../schemas/response/rules_schema.test.ts | 199 +++++++++ .../routes/schemas/response/rules_schema.ts | 149 +++++++ .../routes/schemas/response/schemas.ts | 130 ++++++ .../type_timeline_only_schema.test.ts | 65 +++ .../response/type_timeline_only_schema.ts | 21 + .../routes/schemas/response/utils.test.ts | 130 ++++++ .../routes/schemas/response/utils.ts | 23 + .../schemas/types/iso_date_string.test.ts | 52 +++ .../routes/schemas/types/iso_date_string.ts | 36 ++ .../routes/schemas/types/positive_integer.ts | 26 ++ ...positive_integer_greater_than_zero.test.ts | 48 +++ .../positive_integer_greater_than_zero.ts | 30 ++ .../schemas/types/postive_integer.test.ts | 48 +++ .../types/references_default_array.test.ts | 48 +++ .../schemas/types/references_default_array.ts | 26 ++ .../routes/schemas/types/risk_score.test.ts | 57 +++ .../routes/schemas/types/risk_score.ts | 26 ++ .../routes/schemas/types/uuid.test.ts | 41 ++ .../routes/schemas/types/uuid.ts | 28 ++ .../lib/detection_engine/routes/utils.ts | 27 +- .../rules/get_export_all.test.ts | 2 +- .../rules/get_export_by_object_ids.test.ts | 3 +- .../lib/detection_engine/rules/types.ts | 10 +- .../siem/server/lib/detection_engine/types.ts | 4 +- .../security_and_spaces/tests/find_rules.ts | 6 +- 66 files changed, 3313 insertions(+), 123 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 2b50011cf4dff..1ce41269f455f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -51,7 +51,6 @@ export const mockPrepackagedRule = (): PrepackagedRules => ({ tags: [], version: 1, false_positives: [], - saved_id: 'some-id', max_signals: 100, timeline_id: 'timeline-id', timeline_title: 'timeline-title', @@ -312,7 +311,6 @@ export const getResult = (): RuleAlertType => ({ query: 'user.name: root or user.name: admin', language: 'kuery', outputIndex: '.siem-signals', - savedId: 'some-id', timelineId: 'some-timeline-id', timelineTitle: 'some-timeline-title', meta: { someMeta: 'someField' }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index c4d0489486ef8..baed193036bf9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -17,6 +17,11 @@ import { updatePrepackagedRules } from '../../rules/update_prepacked_rules'; import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; +import { + PrePackagedRulesSchema, + prePackagedRulesSchema, +} from '../schemas/response/prepackaged_rules_schema'; +import { validate } from './validate'; export const createAddPrepackedRulesRoute = ( config: LegacyServices['config'], @@ -78,10 +83,21 @@ export const createAddPrepackedRulesRoute = ( rulesToUpdate, spaceIndex ); - return { + const prepackagedRulesOutput: PrePackagedRulesSchema = { rules_installed: rulesToInstall.length, rules_updated: rulesToUpdate.length, }; + const [validated, errors] = validate(prepackagedRulesOutput, prePackagedRulesSchema); + if (errors != null) { + return headers + .response({ + message: errors, + status_code: 500, + }) + .code(500); + } else { + return validated; + } } catch (err) { const error = transformError(err); return headers diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 08a0589389966..ed0963ae0763e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -13,10 +13,12 @@ import { LegacyServices } from '../../../../types'; import { createRules } from '../../rules/create_rules'; import { BulkRulesRequest } from '../../rules/types'; import { readRules } from '../../rules/read_rules'; -import { transformOrBulkError, getDuplicates } from './utils'; +import { getDuplicates } from './utils'; +import { transformValidateBulkError, validate } from './validate'; import { getIndexExists } from '../../index/get_index_exists'; import { getIndex, transformBulkError, createBulkErrorObject } from '../utils'; import { createRulesBulkSchema } from '../schemas/create_rules_bulk_schema'; +import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; export const createCreateRulesBulkRoute = ( config: LegacyServices['config'], @@ -128,13 +130,13 @@ export const createCreateRulesBulkRoute = ( references, version, }); - return transformOrBulkError(ruleIdOrUuid, createdRule); + return transformValidateBulkError(ruleIdOrUuid, createdRule); } catch (err) { return transformBulkError(ruleIdOrUuid, err); } }) ); - return [ + const rulesBulk = [ ...rules, ...dupes.map(ruleId => createBulkErrorObject({ @@ -144,6 +146,17 @@ export const createCreateRulesBulkRoute = ( }) ), ]; + const [validated, errors] = validate(rulesBulk, rulesBulkSchema); + if (errors != null) { + return headers + .response({ + message: errors, + status_code: 500, + }) + .code(500); + } else { + return validated; + } }, }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 5ad43e70f2651..1898bb1831898 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -160,7 +160,7 @@ describe('create_rules', () => { clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); jest.spyOn(utils, 'transform').mockReturnValue(null); const { payload, statusCode } = await server.inject(getCreateRequest()); - expect(JSON.parse(payload).message).toBe('Internal error transforming rules'); + expect(JSON.parse(payload).message).toBe('Internal error transforming'); expect(statusCode).toBe(500); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index 19e772165628d..8de79614c8a0b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -15,7 +15,8 @@ import { RulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../r import { createRulesSchema } from '../schemas/create_rules_schema'; import { readRules } from '../../rules/read_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { transform } from './utils'; +import { transformValidate } from './validate'; + import { getIndexExists } from '../../index/get_index_exists'; import { getIndex, transformError } from '../utils'; @@ -136,16 +137,16 @@ export const createCreateRulesRoute = ( search: `${createdRule.id}`, searchFields: ['alertId'], }); - const transformed = transform(createdRule, ruleStatuses.saved_objects[0]); - if (transformed == null) { + const [validated, errors] = transformValidate(createdRule, ruleStatuses.saved_objects[0]); + if (errors != null) { return headers .response({ - message: 'Internal error transforming rules', + message: errors, status_code: 500, }) .code(500); } else { - return transformed; + return validated; } } catch (err) { const error = transformError(err); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index aabf3e513bfea..5dae82e3fd0ac 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -10,11 +10,13 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { LegacyServices } from '../../../../types'; import { GetScopedClients } from '../../../../services'; import { queryRulesBulkSchema } from '../schemas/query_rules_bulk_schema'; -import { transformOrBulkError, getIdBulkError } from './utils'; +import { getIdBulkError } from './utils'; +import { transformValidateBulkError, validate } from './validate'; import { transformBulkError } from '../utils'; import { QueryBulkRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { deleteRules } from '../../rules/delete_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; export const createDeleteRulesBulkRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { @@ -58,7 +60,7 @@ export const createDeleteRulesBulkRoute = (getClients: GetScopedClients): Hapi.S ruleStatuses.saved_objects.forEach(async obj => savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) ); - return transformOrBulkError(idOrRuleIdOrUnknown, rule, ruleStatuses); + return transformValidateBulkError(idOrRuleIdOrUnknown, rule, ruleStatuses); } else { return getIdBulkError({ id, ruleId }); } @@ -67,7 +69,17 @@ export const createDeleteRulesBulkRoute = (getClients: GetScopedClients): Hapi.S } }) ); - return rules; + const [validated, errors] = validate(rules, rulesBulkSchema); + if (errors != null) { + return headers + .response({ + message: errors, + status_code: 500, + }) + .code(500); + } else { + return validated; + } }, }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index 57c7c85976619..1d9197e531398 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -89,7 +89,7 @@ describe('delete_rules', () => { clients.savedObjectsClient.delete.mockResolvedValue({}); jest.spyOn(utils, 'transform').mockReturnValue(null); const { payload, statusCode } = await server.inject(getDeleteRequest()); - expect(JSON.parse(payload).message).toBe('Internal error transforming rules'); + expect(JSON.parse(payload).message).toBe('Internal error transforming'); expect(statusCode).toBe(500); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts index 340782523b724..53c76ad0b76f0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -11,7 +11,9 @@ import { deleteRules } from '../../rules/delete_rules'; import { LegacyServices, LegacyRequest } from '../../../../types'; import { GetScopedClients } from '../../../../services'; import { queryRulesSchema } from '../schemas/query_rules_schema'; -import { getIdError, transform } from './utils'; +import { getIdError } from './utils'; +import { transformValidate } from './validate'; + import { transformError } from '../utils'; import { QueryRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; @@ -57,16 +59,16 @@ export const createDeleteRulesRoute = (getClients: GetScopedClients): Hapi.Serve ruleStatuses.saved_objects.forEach(async obj => savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) ); - const transformed = transform(rule, ruleStatuses.saved_objects[0]); - if (transformed == null) { + const [validated, errors] = transformValidate(rule, ruleStatuses.saved_objects[0]); + if (errors != null) { return headers .response({ - message: 'Internal error transforming rules', + message: errors, status_code: 500, }) .code(500); } else { - return transformed; + return validated; } } else { const error = getIdError({ id, ruleId }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts index 019424ea2420a..1baa2b7031581 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts @@ -70,7 +70,7 @@ describe('find_rules', () => { jest.spyOn(utils, 'transformFindAlerts').mockReturnValue(null); const { payload, statusCode } = await server.inject(getFindRequest()); expect(statusCode).toBe(500); - expect(JSON.parse(payload).message).toBe('unknown data type, error transforming alert'); + expect(JSON.parse(payload).message).toBe('Internal error transforming'); }); test('catch error when findRules function throws error', async () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts index 4297e4aebfd58..04b7f2cfe3bec 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -11,7 +11,8 @@ import { GetScopedClients } from '../../../../services'; import { findRules } from '../../rules/find_rules'; import { FindRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { findRulesSchema } from '../schemas/find_rules_schema'; -import { transformFindAlerts } from './utils'; +import { transformValidateFindAlerts } from './validate'; + import { transformError } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; @@ -59,16 +60,16 @@ export const createFindRulesRoute = (getClients: GetScopedClients): Hapi.ServerR return results; }) ); - const transformed = transformFindAlerts(rules, ruleStatuses); - if (transformed == null) { + const [validated, errors] = transformValidateFindAlerts(rules, ruleStatuses); + if (errors != null) { return headers .response({ - message: 'unknown data type, error transforming alert', + message: errors, status_code: 500, }) .code(500); } else { - return transformed; + return validated; } } catch (err) { const error = transformError(err); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index bee57d6b38127..e9394d1abd849 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -15,6 +15,11 @@ import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { findRules } from '../../rules/find_rules'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; +import { + PrePackagedRulesStatusSchema, + prePackagedRulesStatusSchema, +} from '../schemas/response/prepackaged_rules_status_schema'; +import { validate } from './validate'; export const createGetPrepackagedRulesStatusRoute = ( getClients: GetScopedClients @@ -50,12 +55,23 @@ export const createGetPrepackagedRulesStatusRoute = ( const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules); - return { + const prepackagedRulesStatus: PrePackagedRulesStatusSchema = { rules_custom_installed: customRules.total, rules_installed: prepackagedRules.length, rules_not_installed: rulesToInstall.length, rules_not_updated: rulesToUpdate.length, }; + const [validated, errors] = validate(prepackagedRulesStatus, prePackagedRulesStatusSchema); + if (errors != null) { + return headers + .response({ + message: errors, + status_code: 500, + }) + .code(500); + } else { + return validated; + } } catch (err) { const error = transformError(err); return headers diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index a9358a47f25fc..bfe9ade2954dd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -5,7 +5,7 @@ */ import Hapi from 'hapi'; -import { chunk, isEmpty } from 'lodash/fp'; +import { chunk } from 'lodash/fp'; import { extname } from 'path'; import { Readable } from 'stream'; @@ -16,13 +16,23 @@ import { createRules } from '../../rules/create_rules'; import { ImportRulesRequest } from '../../rules/types'; import { readRules } from '../../rules/read_rules'; import { getIndexExists } from '../../index/get_index_exists'; -import { getIndex, transformError, createBulkErrorObject, ImportRuleResponse } from '../utils'; +import { + getIndex, + createBulkErrorObject, + ImportRuleResponse, + BulkError, + isBulkError, + isImportRegular, + transformError, +} from '../utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { ImportRuleAlertRest } from '../../types'; import { patchRules } from '../../rules/patch_rules'; import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema'; import { getTupleDuplicateErrorsAndUniqueRules } from './utils'; +import { validate } from './validate'; import { GetScopedClients } from '../../../../services'; +import { ImportRulesSchema, importRulesSchema } from '../schemas/response/import_rules_schema'; type PromiseFromStreams = ImportRuleAlertRest | Error; @@ -246,12 +256,30 @@ export const createImportRulesRoute = ( ]; } - const errorsResp = importRuleResponse.filter(resp => !isEmpty(resp.error)); - return { + const errorsResp = importRuleResponse.filter(resp => isBulkError(resp)) as BulkError[]; + const successes = importRuleResponse.filter(resp => { + if (isImportRegular(resp)) { + return resp.status_code === 200; + } else { + return false; + } + }); + const importRules: ImportRulesSchema = { success: errorsResp.length === 0, - success_count: importRuleResponse.filter(resp => resp.status_code === 200).length, + success_count: successes.length, errors: errorsResp, }; + const [validated, errors] = validate(importRules, importRulesSchema); + if (errors != null) { + return headers + .response({ + message: errors, + status_code: 500, + }) + .code(500); + } else { + return validated; + } } catch (exc) { const error = transformError(exc); return headers diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index d3f92e9e05bcc..d616b90e6eee0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -13,11 +13,13 @@ import { } from '../../rules/types'; import { LegacyServices } from '../../../../types'; import { GetScopedClients } from '../../../../services'; -import { transformOrBulkError, getIdBulkError } from './utils'; +import { getIdBulkError } from './utils'; +import { transformValidateBulkError, validate } from './validate'; import { transformBulkError } from '../utils'; import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema'; import { patchRules } from '../../rules/patch_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; export const createPatchRulesBulkRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { @@ -113,7 +115,7 @@ export const createPatchRulesBulkRoute = (getClients: GetScopedClients): Hapi.Se search: rule.id, searchFields: ['alertId'], }); - return transformOrBulkError(rule.id, rule, ruleStatuses.saved_objects[0]); + return transformValidateBulkError(rule.id, rule, ruleStatuses.saved_objects[0]); } else { return getIdBulkError({ id, ruleId }); } @@ -122,7 +124,17 @@ export const createPatchRulesBulkRoute = (getClients: GetScopedClients): Hapi.Se } }) ); - return rules; + const [validated, errors] = validate(rules, rulesBulkSchema); + if (errors != null) { + return headers + .response({ + message: errors, + status_code: 500, + }) + .code(500); + } else { + return validated; + } }, }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index 04cd3a026562f..b95ffce14ea04 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -80,7 +80,7 @@ describe('patch_rules', () => { clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); jest.spyOn(utils, 'transform').mockReturnValue(null); const { payload, statusCode } = await server.inject(getPatchRequest()); - expect(JSON.parse(payload).message).toBe('Internal error transforming rules'); + expect(JSON.parse(payload).message).toBe('Internal error transforming'); expect(statusCode).toBe(500); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts index 761d22b084237..42aeccbd50659 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -12,7 +12,9 @@ import { PatchRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '.. import { patchRulesSchema } from '../schemas/patch_rules_schema'; import { LegacyServices } from '../../../../types'; import { GetScopedClients } from '../../../../services'; -import { getIdError, transform } from './utils'; +import { getIdError } from './utils'; +import { transformValidate } from './validate'; + import { transformError } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; @@ -108,16 +110,16 @@ export const createPatchRulesRoute = (getClients: GetScopedClients): Hapi.Server search: rule.id, searchFields: ['alertId'], }); - const transformed = transform(rule, ruleStatuses.saved_objects[0]); - if (transformed == null) { + const [validated, errors] = transformValidate(rule, ruleStatuses.saved_objects[0]); + if (errors != null) { return headers .response({ - message: 'Internal error transforming rules', + message: errors, status_code: 500, }) .code(500); } else { - return transformed; + return validated; } } else { const error = getIdError({ id, ruleId }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index 0366d3648e1ea..e3659f60b8b4a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -74,7 +74,7 @@ describe('read_signals', () => { clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); jest.spyOn(utils, 'transform').mockReturnValue(null); const { payload, statusCode } = await server.inject(getReadRequest()); - expect(JSON.parse(payload).message).toBe('Internal error transforming rules'); + expect(JSON.parse(payload).message).toBe('Internal error transforming'); expect(statusCode).toBe(500); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts index 0180b208d1bb7..c06e7233e7e82 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -6,7 +6,8 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { getIdError, transform } from './utils'; +import { getIdError } from './utils'; +import { transformValidate } from './validate'; import { transformError } from '../utils'; import { readRules } from '../../rules/read_rules'; @@ -53,16 +54,16 @@ export const createReadRulesRoute = (getClients: GetScopedClients): Hapi.ServerR search: rule.id, searchFields: ['alertId'], }); - const transformedOrError = transform(rule, ruleStatuses.saved_objects[0]); - if (transformedOrError == null) { + const [validated, errors] = transformValidate(rule, ruleStatuses.saved_objects[0]); + if (errors != null) { return headers .response({ - message: 'Internal error transforming rules', + message: errors, status_code: 500, }) .code(500); } else { - return transformedOrError; + return validated; } } else { const error = getIdError({ id, ruleId }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 98ed01474c3dc..00a5b06735a32 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -13,11 +13,14 @@ import { } from '../../rules/types'; import { LegacyServices } from '../../../../types'; import { GetScopedClients } from '../../../../services'; -import { transformOrBulkError, getIdBulkError } from './utils'; +import { getIdBulkError } from './utils'; +import { transformValidateBulkError, validate } from './validate'; + import { transformBulkError, getIndex } from '../utils'; import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRules } from '../../rules/update_rules'; +import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; export const createUpdateRulesBulkRoute = ( config: LegacyServices['config'], @@ -120,7 +123,7 @@ export const createUpdateRulesBulkRoute = ( search: rule.id, searchFields: ['alertId'], }); - return transformOrBulkError(rule.id, rule, ruleStatuses.saved_objects[0]); + return transformValidateBulkError(rule.id, rule, ruleStatuses.saved_objects[0]); } else { return getIdBulkError({ id, ruleId }); } @@ -129,7 +132,17 @@ export const createUpdateRulesBulkRoute = ( } }) ); - return rules; + const [validated, errors] = validate(rules, rulesBulkSchema); + if (errors != null) { + return headers + .response({ + message: errors, + status_code: 500, + }) + .code(500); + } else { + return validated; + } }, }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index c3a92ed9a61ae..8a44bfe20f17e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -81,7 +81,7 @@ describe('update_rules', () => { clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); jest.spyOn(utils, 'transform').mockReturnValue(null); const { payload, statusCode } = await server.inject(getUpdateRequest()); - expect(JSON.parse(payload).message).toBe('Internal error transforming rules'); + expect(JSON.parse(payload).message).toBe('Internal error transforming'); expect(statusCode).toBe(500); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index 80fdfc1df8e0e..91a57b5004fdf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -10,7 +10,9 @@ import { UpdateRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '. import { updateRulesSchema } from '../schemas/update_rules_schema'; import { LegacyServices } from '../../../../types'; import { GetScopedClients } from '../../../../services'; -import { getIdError, transform } from './utils'; +import { getIdError } from './utils'; +import { transformValidate } from './validate'; + import { transformError, getIndex } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRules } from '../../rules/update_rules'; @@ -114,16 +116,16 @@ export const createUpdateRulesRoute = ( search: rule.id, searchFields: ['alertId'], }); - const transformed = transform(rule, ruleStatuses.saved_objects[0]); - if (transformed == null) { + const [validated, errors] = transformValidate(rule, ruleStatuses.saved_objects[0]); + if (errors != null) { return headers .response({ - message: 'Internal error transforming rules', + message: errors, status_code: 500, }) .code(500); } else { - return transformed; + return validated; } } else { const error = getIdError({ id, ruleId }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index 5fac3f79f359c..25f0151923e2e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -26,6 +26,8 @@ import { sampleRule } from '../../signals/__mocks__/es_results'; import { getSimpleRule } from '../__mocks__/utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams'; +import { PartialAlert } from '../../../../../../../../plugins/alerting/server'; +import { SanitizedAlert } from '../../../../../../../../plugins/alerting/server/types'; type PromiseFromStreams = ImportRuleAlertRest | Error; @@ -86,7 +88,6 @@ describe('utils', () => { meta: { someMeta: 'someField', }, - saved_id: 'some-id', timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', to: 'now', @@ -149,7 +150,6 @@ describe('utils', () => { meta: { someMeta: 'someField', }, - saved_id: 'some-id', timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', to: 'now', @@ -214,7 +214,6 @@ describe('utils', () => { meta: { someMeta: 'someField', }, - saved_id: 'some-id', timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', to: 'now', @@ -279,7 +278,6 @@ describe('utils', () => { meta: { someMeta: 'someField', }, - saved_id: 'some-id', timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', to: 'now', @@ -342,7 +340,6 @@ describe('utils', () => { meta: { someMeta: 'someField', }, - saved_id: 'some-id', timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', to: 'now', @@ -408,7 +405,6 @@ describe('utils', () => { meta: { someMeta: 'someField', }, - saved_id: 'some-id', timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', to: 'now', @@ -474,7 +470,6 @@ describe('utils', () => { meta: { someMeta: 'someField', }, - saved_id: 'some-id', timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', to: 'now', @@ -540,7 +535,6 @@ describe('utils', () => { meta: { someMeta: 'someField', }, - saved_id: 'some-id', timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', to: 'now', @@ -627,12 +621,15 @@ describe('utils', () => { describe('transformFindAlerts', () => { test('outputs empty data set when data set is empty correct', () => { - const output = transformFindAlerts({ data: [] }); - expect(output).toEqual({ data: [] }); + const output = transformFindAlerts({ data: [], page: 1, perPage: 0, total: 0 }); + expect(output).toEqual({ data: [], page: 1, perPage: 0, total: 0 }); }); test('outputs 200 if the data is of type siem alert', () => { const output = transformFindAlerts({ + page: 1, + perPage: 0, + total: 0, data: [getResult()], }); const expected: OutputRuleAlertRest = { @@ -689,23 +686,31 @@ describe('utils', () => { meta: { someMeta: 'someField', }, - saved_id: 'some-id', timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', version: 1, }; expect(output).toEqual({ + page: 1, + perPage: 0, + total: 0, data: [expected], }); }); test('returns 500 if the data is not of type siem alert', () => { - const output = transformFindAlerts({ data: [{ random: 1 }] }); + const unsafeCast = ([{ name: 'something else' }] as unknown) as SanitizedAlert[]; + const output = transformFindAlerts({ + data: unsafeCast, + page: 1, + perPage: 1, + total: 1, + }); expect(output).toBeNull(); }); }); - describe('transformOrError', () => { + describe('transform', () => { test('outputs 200 if the data is of type siem alert', () => { const output = transform(getResult()); const expected: OutputRuleAlertRest = { @@ -762,7 +767,6 @@ describe('utils', () => { meta: { someMeta: 'someField', }, - saved_id: 'some-id', timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', version: 1, @@ -771,7 +775,8 @@ describe('utils', () => { }); test('returns 500 if the data is not of type siem alert', () => { - const output = transform({ data: [{ random: 1 }] }); + const unsafeCast = ({ data: [{ random: 1 }] } as unknown) as PartialAlert; + const output = transform(unsafeCast); expect(output).toBeNull(); }); }); @@ -934,7 +939,6 @@ describe('utils', () => { meta: { someMeta: 'someField', }, - saved_id: 'some-id', timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', version: 1, @@ -943,7 +947,8 @@ describe('utils', () => { }); test('returns 500 if the data is not of type siem alert', () => { - const output = transformOrBulkError('rule-1', { data: [{ random: 1 }] }); + const unsafeCast = ({ name: 'something else' } as unknown) as PartialAlert; + const output = transformOrBulkError('rule-1', unsafeCast); const expected: BulkError = { rule_id: 'rule-1', error: { message: 'Internal error transforming', status_code: 500 }, @@ -1023,7 +1028,6 @@ describe('utils', () => { references: ['http://www.example.com', 'https://ww.example.com'], risk_score: 50, rule_id: 'rule-1', - saved_id: 'some-id', severity: 'high', tags: [], threat: [ @@ -1083,7 +1087,6 @@ describe('utils', () => { references: ['http://www.example.com', 'https://ww.example.com'], risk_score: 50, rule_id: 'rule-1', - saved_id: 'some-id', severity: 'high', tags: [], threat: [ @@ -1132,7 +1135,6 @@ describe('utils', () => { references: ['http://www.example.com', 'https://ww.example.com'], risk_score: 50, rule_id: 'some other id', - saved_id: 'some-id', severity: 'high', tags: [], threat: [ @@ -1194,15 +1196,12 @@ describe('utils', () => { }); test('returns 1 error and success of false if the data is not of type siem alert', () => { - const output = transformOrImportError( - 'rule-1', - { data: [{ random: 1 }] }, - { - success: true, - success_count: 1, - errors: [], - } - ); + const unsafeCast = ({ name: 'something else' } as unknown) as PartialAlert; + const output = transformOrImportError('rule-1', unsafeCast, { + success: true, + success_count: 1, + errors: [], + }); const expected: ImportSuccessError = { success: false, errors: [ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index 7004bf2088ef2..064bd8315969e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -5,8 +5,10 @@ */ import { pickBy, countBy } from 'lodash/fp'; -import { SavedObject } from 'kibana/server'; +import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; import uuid from 'uuid'; + +import { PartialAlert, FindResult } from '../../../../../../../../plugins/alerting/server'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { RuleAlertType, @@ -155,26 +157,38 @@ export const transformAlertsToRules = ( }; export const transformFindAlerts = ( - findResults: { data: unknown[] }, - ruleStatuses?: unknown[] -): unknown | null => { + findResults: FindResult, + ruleStatuses?: Array> +): { + page: number; + perPage: number; + total: number; + data: Array>; +} | null => { if (!ruleStatuses && isAlertTypes(findResults.data)) { - findResults.data = findResults.data.map(alert => transformAlertToRule(alert)); - return findResults; - } - if (isAlertTypes(findResults.data) && isRuleStatusFindTypes(ruleStatuses)) { - findResults.data = findResults.data.map((alert, idx) => - transformAlertToRule(alert, ruleStatuses[idx].saved_objects[0]) - ); - return findResults; + return { + page: findResults.page, + perPage: findResults.perPage, + total: findResults.total, + data: findResults.data.map(alert => transformAlertToRule(alert)), + }; + } else if (isAlertTypes(findResults.data) && isRuleStatusFindTypes(ruleStatuses)) { + return { + page: findResults.page, + perPage: findResults.perPage, + total: findResults.total, + data: findResults.data.map((alert, idx) => + transformAlertToRule(alert, ruleStatuses[idx].saved_objects[0]) + ), + }; } else { return null; } }; export const transform = ( - alert: unknown, - ruleStatus?: unknown + alert: PartialAlert, + ruleStatus?: SavedObject ): Partial | null => { if (!ruleStatus && isAlertType(alert)) { return transformAlertToRule(alert); @@ -188,7 +202,7 @@ export const transform = ( export const transformOrBulkError = ( ruleId: string, - alert: unknown, + alert: PartialAlert, ruleStatus?: unknown ): Partial | BulkError => { if (isAlertType(alert)) { @@ -208,7 +222,7 @@ export const transformOrBulkError = ( export const transformOrImportError = ( ruleId: string, - alert: unknown, + alert: PartialAlert, existingImportSuccessError: ImportSuccessError ): ImportSuccessError => { if (isAlertType(alert)) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts new file mode 100644 index 0000000000000..812552aef0ed8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts @@ -0,0 +1,155 @@ +/* + * 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 { + validate, + transformValidate, + transformValidateFindAlerts, + transformValidateBulkError, +} from './validate'; +import { getResult } from '../__mocks__/request_responses'; +import { FindResult } from '../../../../../../../../plugins/alerting/server'; +import { RulesSchema } from '../schemas/response/rules_schema'; +import { BulkError } from '../utils'; + +export const ruleOutput: RulesSchema = { + created_at: '2019-12-13T16:40:33.400Z', + updated_at: '2019-12-13T16:40:33.400Z', + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + interval: '5m', + rule_id: 'rule-1', + language: 'kuery', + output_index: '.siem-signals', + max_signals: 100, + risk_score: 50, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + version: 1, + filters: [ + { + query: { + match_phrase: { + 'host.name': 'some-host', + }, + }, + }, + ], + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + meta: { + someMeta: 'someField', + }, + timeline_title: 'some-timeline-title', + timeline_id: 'some-timeline-id', +}; + +describe('validate', () => { + describe('validate', () => { + test('it should do a validation correctly', () => { + const schema = t.exact(t.type({ a: t.number })); + const payload = { a: 1 }; + const [validated, errors] = validate(payload, schema); + + expect(validated).toEqual(payload); + expect(errors).toEqual(null); + }); + + test('it should do an in-validation correctly', () => { + const schema = t.exact(t.type({ a: t.number })); + const payload = { a: 'some other value' }; + const [validated, errors] = validate(payload, schema); + + expect(validated).toEqual(null); + expect(errors).toEqual('Invalid value "some other value" supplied to "a"'); + }); + }); + + describe('transformValidate', () => { + test('it should do a validation correctly of a partial alert', () => { + const ruleAlert = getResult(); + const [validated, errors] = transformValidate(ruleAlert); + expect(validated).toEqual(ruleOutput); + expect(errors).toEqual(null); + }); + + test('it should do an in-validation correctly of a partial alert', () => { + const ruleAlert = getResult(); + delete ruleAlert.name; + const [validated, errors] = transformValidate(ruleAlert); + expect(validated).toEqual(null); + expect(errors).toEqual('Invalid value "undefined" supplied to "name"'); + }); + }); + + describe('transformValidateFindAlerts', () => { + test('it should do a validation correctly of a find alert', () => { + const findResult: FindResult = { data: [getResult()], page: 1, perPage: 0, total: 0 }; + const [validated, errors] = transformValidateFindAlerts(findResult); + expect(validated).toEqual({ data: [ruleOutput], page: 1, perPage: 0, total: 0 }); + expect(errors).toEqual(null); + }); + + test('it should do an in-validation correctly of a partial alert', () => { + const findResult: FindResult = { data: [getResult()], page: 1, perPage: 0, total: 0 }; + delete findResult.page; + const [validated, errors] = transformValidateFindAlerts(findResult); + expect(validated).toEqual(null); + expect(errors).toEqual('Invalid value "undefined" supplied to "page"'); + }); + }); + + describe('transformValidateBulkError', () => { + test('it should do a validation correctly of a rule id', () => { + const ruleAlert = getResult(); + const validatedOrError = transformValidateBulkError('rule-1', ruleAlert); + expect(validatedOrError).toEqual(ruleOutput); + }); + + test('it should do an in-validation correctly of a rule id', () => { + const ruleAlert = getResult(); + delete ruleAlert.name; + const validatedOrError = transformValidateBulkError('rule-1', ruleAlert); + const expected: BulkError = { + error: { + message: 'Invalid value "undefined" supplied to "name"', + status_code: 500, + }, + rule_id: 'rule-1', + }; + expect(validatedOrError).toEqual(expected); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts new file mode 100644 index 0000000000000..e654da99fe67b --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts @@ -0,0 +1,119 @@ +/* + * 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 { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as t from 'io-ts'; + +import { PartialAlert, FindResult } from '../../../../../../../../plugins/alerting/server'; +import { formatErrors } from '../schemas/response/utils'; +import { + isAlertType, + IRuleSavedAttributesSavedObjectAttributes, + isRuleStatusFindType, +} from '../../rules/types'; +import { OutputRuleAlertRest } from '../../types'; +import { createBulkErrorObject, BulkError } from '../utils'; +import { rulesSchema, RulesSchema } from '../schemas/response/rules_schema'; +import { exactCheck } from '../schemas/response/exact_check'; +import { transformFindAlerts, transform, transformAlertToRule } from './utils'; +import { findRulesSchema } from '../schemas/response/find_rules_schema'; + +export const transformValidateFindAlerts = ( + findResults: FindResult, + ruleStatuses?: Array> +): [ + { + page: number; + perPage: number; + total: number; + data: Array>; + } | null, + string | null +] => { + const transformed = transformFindAlerts(findResults, ruleStatuses); + if (transformed == null) { + return [null, 'Internal error transforming']; + } else { + const decoded = findRulesSchema.decode(transformed); + const checked = exactCheck(transformed, decoded); + const left = (errors: t.Errors): string[] => formatErrors(errors); + const right = (): string[] => []; + const piped = pipe(checked, fold(left, right)); + if (piped.length === 0) { + return [transformed, null]; + } else { + return [null, piped.join(',')]; + } + } +}; + +export const transformValidate = ( + alert: PartialAlert, + ruleStatus?: SavedObject +): [RulesSchema | null, string | null] => { + const transformed = transform(alert, ruleStatus); + if (transformed == null) { + return [null, 'Internal error transforming']; + } else { + return validate(transformed, rulesSchema); + } +}; + +export const transformValidateBulkError = ( + ruleId: string, + alert: PartialAlert, + ruleStatus?: unknown +): RulesSchema | BulkError => { + if (isAlertType(alert)) { + if (isRuleStatusFindType(ruleStatus) && ruleStatus?.saved_objects.length > 0) { + const transformed = transformAlertToRule(alert, ruleStatus?.saved_objects[0] ?? ruleStatus); + const [validated, errors] = validate(transformed, rulesSchema); + if (errors != null || validated == null) { + return createBulkErrorObject({ + ruleId, + statusCode: 500, + message: errors ?? 'Internal error transforming', + }); + } else { + return validated; + } + } else { + const transformed = transformAlertToRule(alert); + const [validated, errors] = validate(transformed, rulesSchema); + if (errors != null || validated == null) { + return createBulkErrorObject({ + ruleId, + statusCode: 500, + message: errors ?? 'Internal error transforming', + }); + } else { + return validated; + } + } + } else { + return createBulkErrorObject({ + ruleId, + statusCode: 500, + message: 'Internal error transforming', + }); + } +}; + +export const validate = ( + obj: object, + schema: T +): [t.TypeOf | null, string | null] => { + const decoded = schema.decode(obj); + const checked = exactCheck(obj, decoded); + const left = (errors: t.Errors): [T | null, string | null] => [ + null, + formatErrors(errors).join(','), + ]; + const right = (output: T): [T | null, string | null] => [output, null]; + return pipe(checked, fold(left, right)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts new file mode 100644 index 0000000000000..05b85ffab7263 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts @@ -0,0 +1,100 @@ +/* + * 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 { fold } from 'fp-ts/lib/Either'; +import { RulesSchema } from '../rules_schema'; +import { RulesBulkSchema } from '../rules_bulk_schema'; +import { ErrorSchema } from '../error_schema'; +import { FindRulesSchema } from '../find_rules_schema'; +import { formatErrors } from '../utils'; +import { pipe } from 'fp-ts/lib/pipeable'; + +interface Message { + errors: t.Errors; + schema: T | {}; +} + +const onLeft = (errors: t.Errors): Message => { + return { schema: {}, errors }; +}; + +const onRight = (schema: T): Message => { + return { + schema, + errors: [], + }; +}; + +export const foldLeftRight = fold(onLeft, onRight); + +export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; + +export const getBaseResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesSchema => ({ + id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', + created_at: new Date(anchorDate).toISOString(), + updated_at: new Date(anchorDate).toISOString(), + created_by: 'elastic', + description: 'some description', + enabled: true, + false_positives: ['false positive 1', 'false positive 2'], + from: 'now-6m', + immutable: false, + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + references: ['test 1', 'test 2'], + severity: 'high', + updated_by: 'elastic_kibana', + tags: [], + to: 'now', + type: 'query', + threat: [], + version: 1, + status: 'succeeded', + status_date: '2020-02-22T16:47:50.047Z', + last_success_at: '2020-02-22T16:47:50.047Z', + last_success_message: 'succeeded', + output_index: '.siem-signals-hassanabad-frank-default', + max_signals: 100, + risk_score: 55, + language: 'kuery', + rule_id: 'query-rule-id', + interval: '5m', +}); + +export const getRulesBulkPayload = (): RulesBulkSchema => [getBaseResponsePayload()]; + +export const getErrorPayload = ( + id: string = '819eded6-e9c8-445b-a647-519aea39e063' +): ErrorSchema => ({ + id, + error: { + status_code: 404, + message: 'id: "819eded6-e9c8-445b-a647-519aea39e063" not found', + }, +}); + +export const getFindResponseSingle = (): FindRulesSchema => ({ + page: 1, + perPage: 1, + total: 1, + data: [getBaseResponsePayload()], +}); + +/** + * Convenience utility to keep the error message handling within tests to be + * very concise. + * @param validation The validation to get the errors from + */ +export const getPaths = (validation: t.Validation): string[] => { + return pipe( + validation, + fold( + errors => formatErrors(errors), + () => ['no errors'] + ) + ); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts new file mode 100644 index 0000000000000..fc1c019ff97b5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts @@ -0,0 +1,405 @@ +/* + * 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 { pipe } from 'fp-ts/lib/pipeable'; +import * as t from 'io-ts'; + +import { + checkTypeDependents, + getDependents, + addSavedId, + addTimelineTitle, +} from './check_type_dependents'; +import { foldLeftRight, getBaseResponsePayload, getPaths } from './__mocks__/utils'; +import { left } from 'fp-ts/lib/Either'; +import { exactCheck } from './exact_check'; +import { RulesSchema } from './rules_schema'; +import { TypeAndTimelineOnly } from './type_timeline_only_schema'; + +describe('check_type_dependents', () => { + describe('checkTypeDependents', () => { + test('it should validate a type of "query" without anything extra', () => { + const payload = getBaseResponsePayload(); + + const decoded = checkTypeDependents(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getBaseResponsePayload(); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate invalid_data for the type', () => { + const payload: Omit & { type: string } = getBaseResponsePayload(); + payload.type = 'invalid_data'; + + const decoded = checkTypeDependents(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid_data" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "query" with a saved_id together', () => { + const payload = getBaseResponsePayload(); + payload.type = 'query'; + payload.saved_id = 'save id 123'; + + const decoded = checkTypeDependents(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "saved_id"']); + expect(message.schema).toEqual({}); + }); + + test('it should validate a type of "saved_query" with a "saved_id" dependent', () => { + const payload = getBaseResponsePayload(); + payload.type = 'saved_query'; + payload.saved_id = 'save id 123'; + + const decoded = checkTypeDependents(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getBaseResponsePayload(); + + expected.type = 'saved_query'; + expected.saved_id = 'save id 123'; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate a type of "saved_query" without a "saved_id" dependent', () => { + const payload = getBaseResponsePayload(); + payload.type = 'saved_query'; + delete payload.saved_id; + + const decoded = checkTypeDependents(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('it should NOT validate a type of "saved_query" when it has extra data', () => { + const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + payload.type = 'saved_query'; + payload.saved_id = 'save id 123'; + payload.invalid_extra_data = 'invalid_extra_data'; + + const decoded = checkTypeDependents(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should validate a type of "timeline_id" if there is a "timeline_title" dependent', () => { + const payload = getBaseResponsePayload(); + payload.timeline_id = 'some timeline id'; + payload.timeline_title = 'some timeline title'; + + const decoded = checkTypeDependents(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getBaseResponsePayload(); + expected.timeline_id = 'some timeline id'; + expected.timeline_title = 'some timeline title'; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate a type of "timeline_id" if there is "timeline_title" dependent when it has extra invalid data', () => { + const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + payload.timeline_id = 'some timeline id'; + payload.timeline_title = 'some timeline title'; + payload.invalid_extra_data = 'invalid_extra_data'; + + const decoded = checkTypeDependents(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "timeline_id" if there is NOT a "timeline_title" dependent', () => { + const payload = getBaseResponsePayload(); + payload.timeline_id = 'some timeline id'; + + const decoded = checkTypeDependents(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "timeline_title"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "timeline_title" if there is NOT a "timeline_id" dependent', () => { + const payload = getBaseResponsePayload(); + payload.timeline_title = 'some timeline title'; + + const decoded = checkTypeDependents(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_title" but there is NOT a "timeline_id"', () => { + const payload = getBaseResponsePayload(); + payload.saved_id = 'some saved id'; + payload.type = 'saved_query'; + payload.timeline_title = 'some timeline title'; + + const decoded = checkTypeDependents(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_id" but there is NOT a "timeline_title"', () => { + const payload = getBaseResponsePayload(); + payload.saved_id = 'some saved id'; + payload.type = 'saved_query'; + payload.timeline_id = 'some timeline id'; + + const decoded = checkTypeDependents(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "timeline_title"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('getDependents', () => { + test('it should validate a type of "query" without anything extra', () => { + const payload = getBaseResponsePayload(); + + const dependents = getDependents(payload); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getBaseResponsePayload(); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate invalid_data for the type', () => { + const payload: Omit & { type: string } = getBaseResponsePayload(); + payload.type = 'invalid_data'; + + const dependents = getDependents((payload as unknown) as TypeAndTimelineOnly); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid_data" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "query" with a saved_id together', () => { + const payload = getBaseResponsePayload(); + payload.type = 'query'; + payload.saved_id = 'save id 123'; + + const dependents = getDependents(payload); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "saved_id"']); + expect(message.schema).toEqual({}); + }); + + test('it should validate a type of "saved_query" with a "saved_id" dependent', () => { + const payload = getBaseResponsePayload(); + payload.type = 'saved_query'; + payload.saved_id = 'save id 123'; + + const dependents = getDependents(payload); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getBaseResponsePayload(); + + expected.type = 'saved_query'; + expected.saved_id = 'save id 123'; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate a type of "saved_query" without a "saved_id" dependent', () => { + const payload = getBaseResponsePayload(); + payload.type = 'saved_query'; + delete payload.saved_id; + + const dependents = getDependents(payload); + const decoded = dependents.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('it should NOT validate a type of "saved_query" when it has extra data', () => { + const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + payload.type = 'saved_query'; + payload.saved_id = 'save id 123'; + payload.invalid_extra_data = 'invalid_extra_data'; + + const dependents = getDependents(payload); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should validate a type of "timeline_id" if there is a "timeline_title" dependent', () => { + const payload = getBaseResponsePayload(); + payload.timeline_id = 'some timeline id'; + payload.timeline_title = 'some timeline title'; + + const dependents = getDependents(payload); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getBaseResponsePayload(); + expected.timeline_id = 'some timeline id'; + expected.timeline_title = 'some timeline title'; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate a type of "timeline_id" if there is "timeline_title" dependent when it has extra invalid data', () => { + const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + payload.timeline_id = 'some timeline id'; + payload.timeline_title = 'some timeline title'; + payload.invalid_extra_data = 'invalid_extra_data'; + + const dependents = getDependents(payload); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "timeline_id" if there is NOT a "timeline_title" dependent', () => { + const payload = getBaseResponsePayload(); + payload.timeline_id = 'some timeline id'; + + const dependents = getDependents(payload); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "timeline_title"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "timeline_title" if there is NOT a "timeline_id" dependent', () => { + const payload = getBaseResponsePayload(); + payload.timeline_title = 'some timeline title'; + + const dependents = getDependents(payload); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_title" but there is NOT a "timeline_id"', () => { + const payload = getBaseResponsePayload(); + payload.saved_id = 'some saved id'; + payload.type = 'saved_query'; + payload.timeline_title = 'some timeline title'; + + const decoded = checkTypeDependents(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_id" but there is NOT a "timeline_title"', () => { + const payload = getBaseResponsePayload(); + payload.saved_id = 'some saved id'; + payload.type = 'saved_query'; + payload.timeline_id = 'some timeline id'; + + const dependents = getDependents(payload); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "timeline_title"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('addSavedId', () => { + test('should return empty array if not given a type of "saved_query"', () => { + const emptyArray = addSavedId({ type: 'query' }); + const expected: t.Mixed[] = []; + expect(emptyArray).toEqual(expected); + }); + + test('should array of size 1 given a "saved_query"', () => { + const array = addSavedId({ type: 'saved_query' }); + expect(array.length).toEqual(1); + }); + }); + + describe('addTimelineTitle', () => { + test('should return empty array if not given a timeline_id', () => { + const emptyArray = addTimelineTitle({ type: 'query' }); + const expected: t.Mixed[] = []; + expect(emptyArray).toEqual(expected); + }); + + test('should array of size 2 given a "timeline_id" that is not null', () => { + const array = addTimelineTitle({ type: 'query', timeline_id: 'some id' }); + expect(array.length).toEqual(2); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts new file mode 100644 index 0000000000000..09142c8568b2d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts @@ -0,0 +1,70 @@ +/* + * 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, left, fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { + dependentRulesSchema, + RequiredRulesSchema, + partialRulesSchema, + requiredRulesSchema, +} from './rules_schema'; +import { typeAndTimelineOnlySchema, TypeAndTimelineOnly } from './type_timeline_only_schema'; + +export const addSavedId = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (typeAndTimelineOnly.type === 'saved_query') { + return [t.exact(t.type({ saved_id: dependentRulesSchema.props.saved_id }))]; + } else { + return []; + } +}; + +export const addTimelineTitle = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (typeAndTimelineOnly.timeline_id != null) { + return [ + t.exact(t.type({ timeline_title: dependentRulesSchema.props.timeline_title })), + t.exact(t.type({ timeline_id: dependentRulesSchema.props.timeline_id })), + ]; + } else { + return []; + } +}; + +export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed => { + const dependents: t.Mixed[] = [ + t.exact(requiredRulesSchema), + t.exact(partialRulesSchema), + ...addSavedId(typeAndTimelineOnly), + ...addTimelineTitle(typeAndTimelineOnly), + ]; + + if (dependents.length > 1) { + // This unsafe cast is because t.intersection does not use an array but rather a set of + // tuples and really does not look like they expected us to ever dynamically build up + // intersections, but here we are doing that. Looking at their code, although they limit + // the array elements to 5, it looks like you have N number of intersections + const unsafeCast: [t.Mixed, t.Mixed] = dependents as [t.Mixed, t.Mixed]; + return t.intersection(unsafeCast); + } else { + // We are not allowed to call t.intersection with a single value so we return without + // it here normally. + return dependents[0]; + } +}; + +export const checkTypeDependents = (input: unknown): Either => { + const typeOnlyDecoded = typeAndTimelineOnlySchema.decode(input); + const onLeft = (errors: t.Errors): Either => left(errors); + const onRight = ( + typeAndTimelineOnly: TypeAndTimelineOnly + ): Either => { + const intersections = getDependents(typeAndTimelineOnly); + return intersections.decode(input); + }; + return pipe(typeOnlyDecoded, fold(onLeft, onRight)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts new file mode 100644 index 0000000000000..9708c928870f5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts @@ -0,0 +1,59 @@ +/* + * 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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck } from './exact_check'; +import { foldLeftRight, getErrorPayload, getPaths } from './__mocks__/utils'; +import { errorSchema, ErrorSchema } from './error_schema'; + +describe('error_schema', () => { + test('it should validate an error with a UUID given for id', () => { + const error = getErrorPayload(); + const decoded = errorSchema.decode(getErrorPayload()); + const checked = exactCheck(error, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(error); + }); + + test('it should validate an error with a plain string given for id since sometimes we echo the user id which might not be a UUID back out to them', () => { + const error = getErrorPayload('fake id'); + const decoded = errorSchema.decode(error); + const checked = exactCheck(error, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(error); + }); + + test('it should NOT validate an error when it has extra data next to a valid payload element', () => { + type InvalidError = ErrorSchema & { invalid_extra_data?: string }; + const error: InvalidError = getErrorPayload(); + error.invalid_extra_data = 'invalid_extra_data'; + const decoded = errorSchema.decode(error); + const checked = exactCheck(error, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an error when it has required elements deleted from it', () => { + const error = getErrorPayload(); + delete error.error; + const decoded = errorSchema.decode(error); + const checked = exactCheck(error, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "error"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.ts new file mode 100644 index 0000000000000..f9c776e3b3cdc --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.ts @@ -0,0 +1,27 @@ +/* + * 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'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { rule_id, status_code, message } from './schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +// We use id: t.string intentionally and _never_ the id from global schemas as +// sometimes echo back out the id that the user gave us and it is not guaranteed +// to be a UUID but rather just a string +const partial = t.exact(t.partial({ id: t.string, rule_id })); +const required = t.exact( + t.type({ + error: t.type({ + status_code, + message, + }), + }) +); + +export const errorSchema = t.intersection([partial, required]); +export type ErrorSchema = t.TypeOf; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts new file mode 100644 index 0000000000000..d01c5e19d4322 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts @@ -0,0 +1,146 @@ +/* + * 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 { left, right } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { foldLeftRight, getPaths } from './__mocks__/utils'; +import { exactCheck, findDifferencesRecursive } from './exact_check'; + +describe('exact_check', () => { + test('it returns an error if given extra object properties', () => { + const someType = t.exact( + t.type({ + a: t.string, + }) + ); + const payload = { a: 'test', b: 'test' }; + const decoded = someType.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "b"']); + expect(message.schema).toEqual({}); + }); + + test('it returns an error if the data type is not as expected', () => { + const someType = t.exact( + t.type({ + a: t.string, + }) + ); + const payload = { a: 1 }; + const decoded = someType.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "a"']); + expect(message.schema).toEqual({}); + }); + + test('it does NOT return an error if given normal object properties', () => { + const someType = t.exact( + t.type({ + a: t.string, + }) + ); + const payload = { a: 'test' }; + const decoded = someType.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will return an existing error and not validate', () => { + const payload = { a: 'test' }; + const validationError: t.ValidationError = { + value: 'Some existing error', + context: [], + message: 'some error', + }; + const error: t.Errors = [validationError]; + const leftValue = left(error); + const checked = exactCheck(payload, leftValue); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['some error']); + expect(message.schema).toEqual({}); + }); + + test('it will work with a regular "right" payload without any decoding', () => { + const payload = { a: 'test' }; + const rightValue = right(payload); + const checked = exactCheck(payload, rightValue); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ a: 'test' }); + }); + + test('it should find no differences recursively with two empty objects', () => { + const difference = findDifferencesRecursive({}, {}); + expect(difference).toEqual([]); + }); + + test('it should find a single difference with two objects with different keys', () => { + const difference = findDifferencesRecursive({ a: 1 }, { b: 1 }); + expect(difference).toEqual(['a']); + }); + + test('it should find a two differences with two objects with multiple different keys', () => { + const difference = findDifferencesRecursive({ a: 1, c: 1 }, { b: 1 }); + expect(difference).toEqual(['a', 'c']); + }); + + test('it should find no differences with two objects with the same keys', () => { + const difference = findDifferencesRecursive({ a: 1, b: 1 }, { a: 1, b: 1 }); + expect(difference).toEqual([]); + }); + + test('it should find a difference with two deep objects with different same keys', () => { + const difference = findDifferencesRecursive({ a: 1, b: { c: 1 } }, { a: 1, b: { d: 1 } }); + expect(difference).toEqual(['c']); + }); + + test('it should find a difference within an array', () => { + const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1, b: [{ a: 1 }] }); + expect(difference).toEqual(['c']); + }); + + test('it should find a no difference when using arrays that are identical', () => { + const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1, b: [{ c: 1 }] }); + expect(difference).toEqual([]); + }); + + test('it should find differences when one has an array and the other does not', () => { + const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1 }); + expect(difference).toEqual(['b', '[{"c":1}]']); + }); + + test('it should find differences when one has an deep object and the other does not', () => { + const difference = findDifferencesRecursive({ a: 1, b: { c: 1 } }, { a: 1 }); + expect(difference).toEqual(['b', '{"c":1}']); + }); + + test('it should find differences when one has a deep object with multiple levels and the other does not', () => { + const difference = findDifferencesRecursive({ a: 1, b: { c: { d: 1 } } }, { a: 1 }); + expect(difference).toEqual(['b', '{"c":{"d":1}}']); + }); + + test('it tests two deep objects as the same with no key differences', () => { + const difference = findDifferencesRecursive( + { a: 1, b: { c: { d: 1 } } }, + { a: 1, b: { c: { d: 1 } } } + ); + expect(difference).toEqual([]); + }); + + test('it tests two deep objects with just one deep key difference', () => { + const difference = findDifferencesRecursive( + { a: 1, b: { c: { d: 1 } } }, + { a: 1, b: { c: { e: 1 } } } + ); + expect(difference).toEqual(['d']); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.ts new file mode 100644 index 0000000000000..6fa0472950189 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.ts @@ -0,0 +1,84 @@ +/* + * 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 { left, Either, fold, right } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { isObject, get } from 'lodash/fp'; + +/** + * Given an original object and a decoded object this will return an error + * if and only if the original object has additional keys that the decoded + * object does not have. If the original decoded already has an error, then + * this will return the error as is and not continue. + * + * NOTE: You MUST use t.exact(...) for this to operate correctly as your schema + * needs to remove additional keys before the compare + * + * You might not need this in the future if the below issue is solved: + * https://github.com/gcanti/io-ts/issues/322 + * + * @param original The original to check if it has additional keys + * @param decoded The decoded either which has either an existing error or the + * decoded object which could have additional keys stripped from it. + */ +export const exactCheck = ( + original: object, + decoded: Either +): Either => { + const onLeft = (errors: t.Errors): Either => left(errors); + const onRight = (decodedValue: T): Either => { + const differences = findDifferencesRecursive(original, decodedValue); + if (differences.length !== 0) { + const validationError: t.ValidationError = { + value: differences, + context: [], + message: `invalid keys "${differences.join(',')}"`, + }; + const error: t.Errors = [validationError]; + return left(error); + } else { + return right(decodedValue); + } + }; + return pipe(decoded, fold(onLeft, onRight)); +}; + +export const findDifferencesRecursive = (original: object, decodedValue: T): string[] => { + if (decodedValue == null) { + try { + // It is null and painful when the original contains an object or an array + // the the decoded value does not have. + return [JSON.stringify(original)]; + } catch (err) { + return ['circular reference']; + } + } + const decodedKeys = Object.keys(decodedValue); + const differences = Object.keys(original).flatMap(originalKey => { + const foundKey = decodedKeys.some(key => key === originalKey); + const topLevelKey = foundKey ? [] : [originalKey]; + // I use lodash to cheat and get an any (not going to lie ;-)) + const valueObjectOrArrayOriginal = get(originalKey, original); + const valueObjectOrArrayDecoded = get(originalKey, decodedValue); + if (isObject(valueObjectOrArrayOriginal)) { + return [ + ...topLevelKey, + ...findDifferencesRecursive(valueObjectOrArrayOriginal, valueObjectOrArrayDecoded), + ]; + } else if (Array.isArray(valueObjectOrArrayOriginal)) { + return [ + ...topLevelKey, + ...valueObjectOrArrayOriginal.flatMap((arrayElement, index) => + findDifferencesRecursive(arrayElement, get(index, valueObjectOrArrayDecoded)) + ), + ]; + } else { + return topLevelKey; + } + }); + return differences; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts new file mode 100644 index 0000000000000..937af223b91ab --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { findRulesSchema, FindRulesSchema } from './find_rules_schema'; +import { exactCheck } from './exact_check'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { + foldLeftRight, + getFindResponseSingle, + getBaseResponsePayload, + getPaths, +} from './__mocks__/utils'; +import { left } from 'fp-ts/lib/Either'; +import { RulesSchema } from './rules_schema'; + +describe('find_rules_schema', () => { + test('it should validate a typical single find rules response', () => { + const payload = getFindResponseSingle(); + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getFindResponseSingle()); + }); + + test('it should validate an empty find rules response', () => { + const payload = getFindResponseSingle(); + payload.data = []; + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + const expected = getFindResponseSingle(); + expected.data = []; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should invalidate a typical single find rules response if it is has an extra property on it', () => { + const payload: FindRulesSchema & { invalid_data?: 'invalid' } = getFindResponseSingle(); + payload.invalid_data = 'invalid'; + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should invalidate a typical single find rules response if the rules are invalid within it', () => { + const payload = getFindResponseSingle(); + const invalidRule: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + invalidRule.invalid_extra_data = 'invalid_data'; + payload.data = [invalidRule]; + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should invalidate a typical single find rules response if the rule is missing a required field such as name', () => { + const payload = getFindResponseSingle(); + const invalidRule = getBaseResponsePayload(); + delete invalidRule.name; + payload.data = [invalidRule]; + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "name"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should invalidate a typical single find rules response if it is missing perPage', () => { + const payload = getFindResponseSingle(); + delete payload.perPage; + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "perPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should invalidate a typical single find rules response if it has a negative perPage number', () => { + const payload = getFindResponseSingle(); + payload.perPage = -1; + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to "perPage"']); + expect(message.schema).toEqual({}); + }); + + test('it should invalidate a typical single find rules response if it has a negative page number', () => { + const payload = getFindResponseSingle(); + payload.page = -1; + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to "page"']); + expect(message.schema).toEqual({}); + }); + + test('it should invalidate a typical single find rules response if it has a negative total', () => { + const payload = getFindResponseSingle(); + payload.total = -1; + const decoded = findRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to "total"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.ts new file mode 100644 index 0000000000000..d7e8a246cfe01 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.ts @@ -0,0 +1,21 @@ +/* + * 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 { rulesSchema } from './rules_schema'; +import { page, perPage, total } from './schemas'; + +export const findRulesSchema = t.exact( + t.type({ + page, + perPage, + total, + data: t.array(rulesSchema), + }) +); + +export type FindRulesSchema = t.TypeOf; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts new file mode 100644 index 0000000000000..62ffcd527eea8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts @@ -0,0 +1,120 @@ +/* + * 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 { exactCheck } from './exact_check'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { foldLeftRight, getPaths } from './__mocks__/utils'; +import { left } from 'fp-ts/lib/Either'; +import { ImportRulesSchema, importRulesSchema } from './import_rules_schema'; +import { ErrorSchema } from './error_schema'; + +describe('import_rules_schema', () => { + test('it should validate an empty import response with no errors', () => { + const payload: ImportRulesSchema = { success: true, success_count: 0, errors: [] }; + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an empty import response with a single error', () => { + const payload: ImportRulesSchema = { + success: false, + success_count: 0, + errors: [{ error: { status_code: 400, message: 'some message' } }], + }; + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an empty import response with two errors', () => { + const payload: ImportRulesSchema = { + success: false, + success_count: 0, + errors: [ + { error: { status_code: 400, message: 'some message' } }, + { error: { status_code: 500, message: 'some message' } }, + ], + }; + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a status_count that is a negative number', () => { + const payload: ImportRulesSchema = { + success: false, + success_count: -1, + errors: [], + }; + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "success_count"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a success that is not a boolean', () => { + const payload: Omit & { success: string } = { + success: 'hello', + success_count: 0, + errors: [], + }; + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "hello" supplied to "success"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a success an extra invalid field', () => { + const payload: ImportRulesSchema & { invalid_field: string } = { + success: true, + success_count: 0, + errors: [], + invalid_field: 'invalid_data', + }; + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_field"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an extra field in the second position of the array', () => { + type InvalidError = ErrorSchema & { invalid_data?: string }; + const payload: Omit & { + errors: InvalidError[]; + } = { + success: true, + success_count: 0, + errors: [ + { error: { status_code: 400, message: 'some message' } }, + { invalid_data: 'something', error: { status_code: 500, message: 'some message' } }, + ], + }; + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_data"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.ts new file mode 100644 index 0000000000000..dec32b18e2b24 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.ts @@ -0,0 +1,22 @@ +/* + * 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'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { success, success_count } from './schemas'; +import { errorSchema } from './error_schema'; +/* eslint-enable @typescript-eslint/camelcase */ + +export const importRulesSchema = t.exact( + t.type({ + success, + success_count, + errors: t.array(errorSchema), + }) +); + +export type ImportRulesSchema = t.TypeOf; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts new file mode 100644 index 0000000000000..7f9b296e2d466 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { exactCheck } from './exact_check'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { foldLeftRight, getPaths } from './__mocks__/utils'; +import { left } from 'fp-ts/lib/Either'; +import { PrePackagedRulesSchema, prePackagedRulesSchema } from './prepackaged_rules_schema'; + +describe('prepackaged_rules_schema', () => { + test('it should validate an empty prepackaged response with defaults', () => { + const payload: PrePackagedRulesSchema = { rules_installed: 0, rules_updated: 0 }; + const decoded = prePackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an extra invalid field added', () => { + const payload: PrePackagedRulesSchema & { invalid_field: string } = { + rules_installed: 0, + rules_updated: 0, + invalid_field: 'invalid', + }; + const decoded = prePackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_field"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an empty prepackaged response with a negative "rules_installed" number', () => { + const payload: PrePackagedRulesSchema = { rules_installed: -1, rules_updated: 0 }; + const decoded = prePackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "rules_installed"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an empty prepackaged response with a negative "rules_updated"', () => { + const payload: PrePackagedRulesSchema = { rules_installed: 0, rules_updated: -1 }; + const decoded = prePackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "rules_updated"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an empty prepackaged response if "rules_installed" is not there', () => { + const payload: PrePackagedRulesSchema = { rules_installed: 0, rules_updated: 0 }; + delete payload.rules_installed; + const decoded = prePackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "rules_installed"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an empty prepackaged response if "rules_updated" is not there', () => { + const payload: PrePackagedRulesSchema = { rules_installed: 0, rules_updated: 0 }; + delete payload.rules_updated; + const decoded = prePackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "rules_updated"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.ts new file mode 100644 index 0000000000000..f0eff0ba19753 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.ts @@ -0,0 +1,20 @@ +/* + * 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'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { rules_installed, rules_updated } from './schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +export const prePackagedRulesSchema = t.exact( + t.type({ + rules_installed, + rules_updated, + }) +); + +export type PrePackagedRulesSchema = t.TypeOf; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts new file mode 100644 index 0000000000000..9d44e09e847a0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts @@ -0,0 +1,133 @@ +/* + * 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 { exactCheck } from './exact_check'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { foldLeftRight, getPaths } from './__mocks__/utils'; +import { left } from 'fp-ts/lib/Either'; +import { + PrePackagedRulesStatusSchema, + prePackagedRulesStatusSchema, +} from './prepackaged_rules_status_schema'; + +describe('prepackaged_rules_schema', () => { + test('it should validate an empty prepackaged response with defaults', () => { + const payload: PrePackagedRulesStatusSchema = { + rules_installed: 0, + rules_not_installed: 0, + rules_not_updated: 0, + rules_custom_installed: 0, + }; + const decoded = prePackagedRulesStatusSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an extra invalid field added', () => { + const payload: PrePackagedRulesStatusSchema & { invalid_field: string } = { + rules_installed: 0, + rules_not_installed: 0, + rules_not_updated: 0, + rules_custom_installed: 0, + invalid_field: 'invalid', + }; + const decoded = prePackagedRulesStatusSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_field"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an empty prepackaged response with a negative "rules_installed" number', () => { + const payload: PrePackagedRulesStatusSchema = { + rules_installed: -1, + rules_not_installed: 0, + rules_not_updated: 0, + rules_custom_installed: 0, + }; + const decoded = prePackagedRulesStatusSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "rules_installed"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an empty prepackaged response with a negative "rules_not_installed"', () => { + const payload: PrePackagedRulesStatusSchema = { + rules_installed: 0, + rules_not_installed: -1, + rules_not_updated: 0, + rules_custom_installed: 0, + }; + const decoded = prePackagedRulesStatusSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "rules_not_installed"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an empty prepackaged response with a negative "rules_not_updated"', () => { + const payload: PrePackagedRulesStatusSchema = { + rules_installed: 0, + rules_not_installed: 0, + rules_not_updated: -1, + rules_custom_installed: 0, + }; + const decoded = prePackagedRulesStatusSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "rules_not_updated"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an empty prepackaged response with a negative "rules_custom_installed"', () => { + const payload: PrePackagedRulesStatusSchema = { + rules_installed: 0, + rules_not_installed: 0, + rules_not_updated: 0, + rules_custom_installed: -1, + }; + const decoded = prePackagedRulesStatusSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "rules_custom_installed"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an empty prepackaged response if "rules_installed" is not there', () => { + const payload: PrePackagedRulesStatusSchema = { + rules_installed: 0, + rules_not_installed: 0, + rules_not_updated: 0, + rules_custom_installed: 0, + }; + delete payload.rules_installed; + const decoded = prePackagedRulesStatusSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "rules_installed"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.ts new file mode 100644 index 0000000000000..72e5821eb4697 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.ts @@ -0,0 +1,27 @@ +/* + * 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'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { + rules_installed, + rules_custom_installed, + rules_not_installed, + rules_not_updated, +} from './schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +export const prePackagedRulesStatusSchema = t.exact( + t.type({ + rules_custom_installed, + rules_installed, + rules_not_installed, + rules_not_updated, + }) +); + +export type PrePackagedRulesStatusSchema = t.TypeOf; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts new file mode 100644 index 0000000000000..c2f346cacc43e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts @@ -0,0 +1,131 @@ +/* + * 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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck } from './exact_check'; +import { + foldLeftRight, + getBaseResponsePayload, + getErrorPayload, + getPaths, +} from './__mocks__/utils'; +import { RulesBulkSchema, rulesBulkSchema } from './rules_bulk_schema'; +import { RulesSchema } from './rules_schema'; +import { ErrorSchema } from './error_schema'; + +describe('prepackaged_rule_schema', () => { + test('it should validate a regular message and and error together with a uuid', () => { + const payload: RulesBulkSchema = [getBaseResponsePayload(), getErrorPayload()]; + const decoded = rulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([getBaseResponsePayload(), getErrorPayload()]); + }); + + test('it should validate a regular message and and error together when the error has a non UUID', () => { + const payload: RulesBulkSchema = [getBaseResponsePayload(), getErrorPayload('fake id')]; + const decoded = rulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([getBaseResponsePayload(), getErrorPayload('fake id')]); + }); + + test('it should validate an error', () => { + const payload: RulesBulkSchema = [getErrorPayload('fake id')]; + const decoded = rulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([getErrorPayload('fake id')]); + }); + + test('it should NOT validate a rule with a deleted value', () => { + const rule = getBaseResponsePayload(); + delete rule.name; + const payload: RulesBulkSchema = [rule]; + const decoded = rulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "name"', + 'Invalid value "undefined" supplied to "error"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an invalid error message with a deleted value', () => { + const error = getErrorPayload('fake id'); + delete error.error; + const payload: RulesBulkSchema = [error]; + const decoded = rulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "type"', + 'Invalid value "undefined" supplied to "error"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "query" when it has extra data', () => { + const rule: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + rule.invalid_extra_data = 'invalid_extra_data'; + const payload: RulesBulkSchema = [rule]; + const decoded = rulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "query" when it has extra data next to a valid error', () => { + const rule: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + rule.invalid_extra_data = 'invalid_extra_data'; + const payload: RulesBulkSchema = [getErrorPayload(), rule]; + const decoded = rulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an error when it has extra data', () => { + type InvalidError = ErrorSchema & { invalid_extra_data?: string }; + const error: InvalidError = getErrorPayload(); + error.invalid_extra_data = 'invalid'; + const payload: RulesBulkSchema = [error]; + const decoded = rulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an error when it has extra data next to a valid payload element', () => { + type InvalidError = ErrorSchema & { invalid_extra_data?: string }; + const error: InvalidError = getErrorPayload(); + error.invalid_extra_data = 'invalid'; + const payload: RulesBulkSchema = [getBaseResponsePayload(), error]; + const decoded = rulesBulkSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.ts new file mode 100644 index 0000000000000..7696c66082aa7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.ts @@ -0,0 +1,13 @@ +/* + * 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 { rulesSchema } from './rules_schema'; +import { errorSchema } from './error_schema'; + +export const rulesBulkSchema = t.array(t.union([rulesSchema, errorSchema])); +export type RulesBulkSchema = t.TypeOf; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts new file mode 100644 index 0000000000000..a2594ffa21c45 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts @@ -0,0 +1,199 @@ +/* + * 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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck } from './exact_check'; +import { rulesSchema, RulesSchema } from './rules_schema'; +import { foldLeftRight, getBaseResponsePayload, getPaths } from './__mocks__/utils'; + +export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; + +describe('rules_schema', () => { + test('it should validate a type of "query" without anything extra', () => { + const payload = getBaseResponsePayload(); + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getBaseResponsePayload(); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate a type of "query" when it has extra data', () => { + const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + payload.invalid_extra_data = 'invalid_extra_data'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate invalid_data for the type', () => { + const payload: Omit & { type: string } = getBaseResponsePayload(); + payload.type = 'invalid_data'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid_data" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "query" with a saved_id together', () => { + const payload = getBaseResponsePayload(); + payload.type = 'query'; + payload.saved_id = 'save id 123'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "saved_id"']); + expect(message.schema).toEqual({}); + }); + + test('it should validate a type of "saved_query" with a "saved_id" dependent', () => { + const payload = getBaseResponsePayload(); + payload.type = 'saved_query'; + payload.saved_id = 'save id 123'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getBaseResponsePayload(); + + expected.type = 'saved_query'; + expected.saved_id = 'save id 123'; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate a type of "saved_query" without a "saved_id" dependent', () => { + const payload = getBaseResponsePayload(); + payload.type = 'saved_query'; + delete payload.saved_id; + + const decoded = rulesSchema.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('it should NOT validate a type of "saved_query" when it has extra data', () => { + const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + payload.type = 'saved_query'; + payload.saved_id = 'save id 123'; + payload.invalid_extra_data = 'invalid_extra_data'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should validate a type of "timeline_id" if there is a "timeline_title" dependent', () => { + const payload = getBaseResponsePayload(); + payload.timeline_id = 'some timeline id'; + payload.timeline_title = 'some timeline title'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getBaseResponsePayload(); + expected.timeline_id = 'some timeline id'; + expected.timeline_title = 'some timeline title'; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate a type of "timeline_id" if there is "timeline_title" dependent when it has extra invalid data', () => { + const payload: RulesSchema & { invalid_extra_data?: string } = getBaseResponsePayload(); + payload.timeline_id = 'some timeline id'; + payload.timeline_title = 'some timeline title'; + payload.invalid_extra_data = 'invalid_extra_data'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "timeline_id" if there is NOT a "timeline_title" dependent', () => { + const payload = getBaseResponsePayload(); + payload.timeline_id = 'some timeline id'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "timeline_title"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "timeline_title" if there is NOT a "timeline_id" dependent', () => { + const payload = getBaseResponsePayload(); + payload.timeline_title = 'some timeline title'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_title" but there is NOT a "timeline_id"', () => { + const payload = getBaseResponsePayload(); + payload.saved_id = 'some saved id'; + payload.type = 'saved_query'; + payload.timeline_title = 'some timeline title'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "timeline_title"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a type of "saved_query" with a "saved_id" dependent and a "timeline_id" but there is NOT a "timeline_title"', () => { + const payload = getBaseResponsePayload(); + payload.saved_id = 'some saved id'; + payload.type = 'saved_query'; + payload.timeline_id = 'some timeline id'; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "timeline_title"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts new file mode 100644 index 0000000000000..ae2d6269279e1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts @@ -0,0 +1,149 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ +import * as t from 'io-ts'; +import { isObject } from 'lodash/fp'; +import { Either } from 'fp-ts/lib/Either'; + +import { checkTypeDependents } from './check_type_dependents'; +import { + description, + enabled, + false_positives, + from, + id, + immutable, + index, + interval, + rule_id, + language, + name, + output_index, + max_signals, + query, + references, + severity, + updated_by, + tags, + to, + risk_score, + created_at, + created_by, + updated_at, + saved_id, + timeline_id, + timeline_title, + type, + threat, + job_status, + status_date, + last_success_at, + last_success_message, + last_failure_at, + last_failure_message, + version, + filters, + meta, +} from './schemas'; + +/** + * This is the required fields for the rules schema response. Put all required properties on + * this base for schemas such as create_rules, update_rules, for the correct validation of the + * output schema. + */ +export const requiredRulesSchema = t.type({ + description, + enabled, + false_positives, + from, + id, + immutable, + interval, + rule_id, + language, + output_index, + max_signals, + risk_score, + name, + query, + references, + severity, + updated_by, + tags, + to, + type, + threat, + created_at, + updated_at, + created_by, + version, +}); + +export type RequiredRulesSchema = t.TypeOf; + +/** + * If you have type dependents or exclusive or situations add them here AND update the + * check_type_dependents file for whichever REST flow it is going through. + */ +export const dependentRulesSchema = t.partial({ + // when type = saved_query, saved_is is required + saved_id, + + // These two are required together or not at all. + timeline_id, + timeline_title, +}); + +/** + * This is the partial or optional fields for the rules schema. Put all optional + * properties on this. DO NOT PUT type dependents such as xor relationships here. + * Instead use dependentRulesSchema and check_type_dependents for how to do those. + */ +export const partialRulesSchema = t.partial({ + status: job_status, + status_date, + last_success_at, + last_success_message, + last_failure_at, + last_failure_message, + filters, + meta, + index, +}); + +/** + * This is the rules schema WITHOUT typeDependents. You don't normally want to use this for a decode + */ +export const rulesWithoutTypeDependentsSchema = t.intersection([ + t.exact(dependentRulesSchema), + t.exact(partialRulesSchema), + t.exact(requiredRulesSchema), +]); +export type RulesWithoutTypeDependentsSchema = t.TypeOf; + +/** + * This is the rulesSchema you want to use for checking type dependents and all the properties + * through: rulesSchema.decode(someJSONObject) + */ +export const rulesSchema = new t.Type< + RulesWithoutTypeDependentsSchema, + RulesWithoutTypeDependentsSchema, + unknown +>( + 'RulesSchema', + (input: unknown): input is RulesWithoutTypeDependentsSchema => isObject(input), + (input): Either => { + return checkTypeDependents(input); + }, + t.identity +); + +/** + * This is the correct type you want to use for Rules that are outputted from the + * REST interface. This has all base and all optional properties merged together. + */ +export type RulesSchema = t.TypeOf; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts new file mode 100644 index 0000000000000..14de14a8464fb --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts @@ -0,0 +1,130 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; +import { RiskScore } from '../types/risk_score'; +import { UUID } from '../types/uuid'; +import { IsoDateString } from '../types/iso_date_string'; +import { PositiveIntegerGreaterThanZero } from '../types/positive_integer_greater_than_zero'; +import { PositiveInteger } from '../types/positive_integer'; + +export const description = t.string; +export const enabled = t.boolean; +export const exclude_export_details = t.boolean; +export const false_positives = t.array(t.string); +export const file_name = t.string; + +/** + * TODO: Right now the filters is an "unknown", when it could more than likely + * become the actual ESFilter as a type. + */ +export const filters = t.array(t.unknown); // Filters are not easily type-able yet + +// TODO: Create a regular expression type or custom date math part type here +export const from = t.string; + +export const immutable = t.boolean; + +// Note: Never make this a strict uuid, we allow the rule_id to be any string at the moment +// in case we encounter 3rd party rule systems which might be using auto incrementing numbers +// or other different things. +export const rule_id = t.string; + +export const id = UUID; +export const index = t.array(t.string); +export const interval = t.string; +export const query = t.string; +export const language = t.keyof({ kuery: null, lucene: null }); +export const objects = t.array(t.type({ rule_id })); +export const output_index = t.string; +export const saved_id = t.string; +export const timeline_id = t.string; +export const timeline_title = t.string; + +/** + * Note that this is a plain unknown object because we allow the UI + * to send us extra additional information as "meta" which can be anything. + * + * TODO: Strip away extra information and possibly even "freeze" this object + * so we have tighter control over 3rd party data structures. + */ +export const meta = t.object; +export const max_signals = PositiveIntegerGreaterThanZero; +export const name = t.string; +export const risk_score = RiskScore; +export const severity = t.keyof({ low: null, medium: null, high: null, critical: null }); +export const status = t.keyof({ open: null, closed: null }); +export const job_status = t.keyof({ succeeded: null, failed: null, 'going to run': null }); + +// TODO: Create a regular expression type or custom date math part type here +export const to = t.string; + +export const type = t.keyof({ query: null, saved_query: null }); +export const queryFilter = t.string; +export const references = t.array(t.string); +export const per_page = PositiveInteger; +export const page = PositiveIntegerGreaterThanZero; +export const signal_ids = t.array(t.string); + +// TODO: Can this be more strict or is this is the set of all Elastic Queries? +export const signal_status_query = t.object; + +export const sort_field = t.string; +export const sort_order = t.keyof({ asc: null, desc: null }); +export const tags = t.array(t.string); +export const fields = t.array(t.string); +export const threat_framework = t.string; +export const threat_tactic_id = t.string; +export const threat_tactic_name = t.string; +export const threat_tactic_reference = t.string; +export const threat_tactic = t.type({ + id: threat_tactic_id, + name: threat_tactic_name, + reference: threat_tactic_reference, +}); +export const threat_technique_id = t.string; +export const threat_technique_name = t.string; +export const threat_technique_reference = t.string; +export const threat_technique = t.exact( + t.type({ + id: threat_technique_id, + name: threat_technique_name, + reference: threat_technique_reference, + }) +); +export const threat_techniques = t.array(threat_technique); +export const threat = t.array( + t.exact( + t.type({ + framework: threat_framework, + tactic: threat_tactic, + technique: threat_techniques, + }) + ) +); +export const created_at = IsoDateString; +export const updated_at = IsoDateString; +export const updated_by = t.string; +export const created_by = t.string; +export const version = PositiveIntegerGreaterThanZero; +export const last_success_at = IsoDateString; +export const last_success_message = t.string; +export const last_failure_at = IsoDateString; +export const last_failure_message = t.string; +export const status_date = IsoDateString; +export const rules_installed = PositiveInteger; +export const rules_updated = PositiveInteger; +export const status_code = PositiveInteger; +export const message = t.string; +export const perPage = PositiveInteger; +export const total = PositiveInteger; +export const success = t.boolean; +export const success_count = PositiveInteger; +export const rules_custom_installed = PositiveInteger; +export const rules_not_installed = PositiveInteger; +export const rules_not_updated = PositiveInteger; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts new file mode 100644 index 0000000000000..219cd68d3a2a1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts @@ -0,0 +1,65 @@ +/* + * 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 { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck } from './exact_check'; +import { foldLeftRight, getPaths } from './__mocks__/utils'; +import { TypeAndTimelineOnly, typeAndTimelineOnlySchema } from './type_timeline_only_schema'; + +describe('prepackaged_rule_schema', () => { + test('it should validate a a type and timeline_id together', () => { + const payload: TypeAndTimelineOnly = { + type: 'query', + timeline_id: 'some id', + }; + const decoded = typeAndTimelineOnlySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate just a type without a timeline_id of type query', () => { + const payload: TypeAndTimelineOnly = { + type: 'query', + }; + const decoded = typeAndTimelineOnlySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate just a type of saved_query', () => { + const payload: TypeAndTimelineOnly = { + type: 'saved_query', + }; + const decoded = typeAndTimelineOnlySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate an invalid type', () => { + const payload: Omit & { type: string } = { + type: 'some other type', + }; + const decoded = typeAndTimelineOnlySchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some other type" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.ts new file mode 100644 index 0000000000000..6d11ff03563d1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.ts @@ -0,0 +1,21 @@ +/* + * 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'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { timeline_id, type } from './schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +/** + * Special schema type that is only the type and the timeline_id. + * This is used for dependent type checking only. + */ +export const typeAndTimelineOnlySchema = t.intersection([ + t.exact(t.type({ type })), + t.exact(t.partial({ timeline_id })), +]); +export type TypeAndTimelineOnly = t.TypeOf; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts new file mode 100644 index 0000000000000..cd223c24792bf --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { formatErrors } from './utils'; + +describe('utils', () => { + test('returns an empty error message string if there are no errors', () => { + const errors: t.Errors = []; + const output = formatErrors(errors); + expect(output).toEqual([]); + }); + + test('returns a single error message if given one', () => { + const validationError: t.ValidationError = { + value: 'Some existing error', + context: [], + message: 'some error', + }; + const errors: t.Errors = [validationError]; + const output = formatErrors(errors); + expect(output).toEqual(['some error']); + }); + + test('returns a two error messages if given two', () => { + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context: [], + message: 'some error 1', + }; + const validationError2: t.ValidationError = { + value: 'Some existing error 2', + context: [], + message: 'some error 2', + }; + const errors: t.Errors = [validationError1, validationError2]; + const output = formatErrors(errors); + expect(output).toEqual(['some error 1', 'some error 2']); + }); + + test('will use message before context if it is set', () => { + const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + message: 'I should be used first', + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual(['I should be used first']); + }); + + test('will use context entry of a single string', () => { + const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "some string key"']); + }); + + test('will use two context entries of two strings', () => { + const context: t.Context = ([ + { key: 'some string key 1' }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 1,some string key 2"', + ]); + }); + + test('will filter out and not use any strings of numbers', () => { + const context: t.Context = ([ + { key: '5' }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 2"', + ]); + }); + + test('will filter out and not use null', () => { + const context: t.Context = ([ + { key: null }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 2"', + ]); + }); + + test('will filter out and not use empty strings', () => { + const context: t.Context = ([ + { key: '' }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 2"', + ]); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.ts new file mode 100644 index 0000000000000..a9c222050ee38 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.ts @@ -0,0 +1,23 @@ +/* + * 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'; + +export const formatErrors = (errors: t.Errors): string[] => { + return errors.map(error => { + if (error.message != null) { + return error.message; + } else { + const mappedContext = error.context + .filter( + entry => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== '' + ) + .map(entry => entry.key) + .join(','); + return `Invalid value "${error.value}" supplied to "${mappedContext}"`; + } + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts new file mode 100644 index 0000000000000..fbafaf7f52ecb --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts @@ -0,0 +1,52 @@ +/* + * 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 { IsoDateString } from './iso_date_string'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; +import { left } from 'fp-ts/lib/Either'; + +describe('ios_date_string', () => { + test('it should validate a iso string', () => { + const payload = '2020-02-26T00:32:34.541Z'; + const decoded = IsoDateString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an epoch number', () => { + const payload = '1582677283067'; + const decoded = IsoDateString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1582677283067" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a number such as 2000', () => { + const payload = '2000'; + const decoded = IsoDateString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "2000" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a UTC', () => { + const payload = 'Wed, 26 Feb 2020 00:36:20 GMT'; + const decoded = IsoDateString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "Wed, 26 Feb 2020 00:36:20 GMT" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.ts new file mode 100644 index 0000000000000..d63c18154c5d4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.ts @@ -0,0 +1,36 @@ +/* + * 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 type IsoDateStringC = t.Type; + +/** + * Types the IsoDateString as: + * - A string that is an ISOString + */ +export const IsoDateString: IsoDateStringC = new t.Type( + 'IsoDateString', + t.string.is, + (input, context): Either => { + if (typeof input === 'string') { + try { + const parsed = new Date(input); + if (parsed.toISOString() === input) { + return t.success(input); + } else { + return t.failure(input, context); + } + } catch (err) { + return t.failure(input, context); + } + } else { + return t.failure(input, context); + } + }, + t.identity +); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer.ts new file mode 100644 index 0000000000000..ac98dd6f5d85c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer.ts @@ -0,0 +1,26 @@ +/* + * 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 type PositiveIntegerC = t.Type; + +/** + * Types the positive integer are: + * - Natural Number (positive integer and not a float), + * - zero or greater + */ +export const PositiveInteger: PositiveIntegerC = new t.Type( + 'PositiveInteger', + t.number.is, + (input, context): Either => { + return typeof input === 'number' && Number.isSafeInteger(input) && input >= 0 + ? t.success(input) + : t.failure(input, context); + }, + t.identity +); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts new file mode 100644 index 0000000000000..bc17303f24203 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; +import { left } from 'fp-ts/lib/Either'; + +describe('positive_integer_greater_than_zero', () => { + test('it should validate a positive number', () => { + const payload = 1; + const decoded = PositiveIntegerGreaterThanZero.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a zero', () => { + const payload = 0; + const decoded = PositiveIntegerGreaterThanZero.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a negative number', () => { + const payload = -1; + const decoded = PositiveIntegerGreaterThanZero.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a string', () => { + const payload = 'some string'; + const decoded = PositiveIntegerGreaterThanZero.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "some string" supplied to ""']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.ts new file mode 100644 index 0000000000000..861ec07854c1c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.ts @@ -0,0 +1,30 @@ +/* + * 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 type PositiveIntegerGreaterThanZeroC = t.Type; + +/** + * Types the positive integer greater than zero is: + * - Natural Number (positive integer and not a float), + * - 1 or greater + */ +export const PositiveIntegerGreaterThanZero: PositiveIntegerGreaterThanZeroC = new t.Type< + number, + number, + unknown +>( + 'PositiveIntegerGreaterThanZero', + t.number.is, + (input, context): Either => { + return typeof input === 'number' && Number.isSafeInteger(input) && input >= 1 + ? t.success(input) + : t.failure(input, context); + }, + t.identity +); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts new file mode 100644 index 0000000000000..cee451279663a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { PositiveInteger } from './positive_integer'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; +import { left } from 'fp-ts/lib/Either'; + +describe('positive_integer_greater_than_zero', () => { + test('it should validate a positive number', () => { + const payload = 1; + const decoded = PositiveInteger.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a zero', () => { + const payload = 0; + const decoded = PositiveInteger.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a negative number', () => { + const payload = -1; + const decoded = PositiveInteger.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a string', () => { + const payload = 'some string'; + const decoded = PositiveInteger.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "some string" supplied to ""']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts new file mode 100644 index 0000000000000..3ae8415b4f170 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { ReferencesDefaultArray } from './references_default_array'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; +import { left } from 'fp-ts/lib/Either'; + +describe('references_default_array', () => { + test('it should validate an empty array', () => { + const payload: string[] = []; + const decoded = ReferencesDefaultArray.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 strings', () => { + const payload = ['value 1', 'value 2']; + const decoded = ReferencesDefaultArray.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 = ['value 1', 5]; + const decoded = ReferencesDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = ReferencesDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.ts new file mode 100644 index 0000000000000..57a7ed4e4d456 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.ts @@ -0,0 +1,26 @@ +/* + * 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 type ReferencesDefaultArrayC = t.Type; + +/** + * Types the ReferencesDefaultArray as: + * - If null or undefined, then a default array will be set + */ +export const ReferencesDefaultArray: ReferencesDefaultArrayC = new t.Type< + string[], + string[], + unknown +>( + 'referencesWithDefaultArray', + t.array(t.string).is, + (input): Either => + input == null ? t.success([]) : t.array(t.string).decode(input), + t.identity +); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts new file mode 100644 index 0000000000000..ab3f80944489f --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts @@ -0,0 +1,57 @@ +/* + * 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 { RiskScore } from './risk_score'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; +import { left } from 'fp-ts/lib/Either'; + +describe('risk_score', () => { + test('it should validate a positive number', () => { + const payload = 1; + const decoded = RiskScore.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a zero', () => { + const payload = 0; + const decoded = RiskScore.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a negative number', () => { + const payload = -1; + const decoded = RiskScore.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a string', () => { + const payload = 'some string'; + const decoded = RiskScore.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "some string" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a risk score greater than 100', () => { + const payload = 101; + const decoded = RiskScore.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "101" supplied to ""']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.ts new file mode 100644 index 0000000000000..c44fae3c6f3e7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.ts @@ -0,0 +1,26 @@ +/* + * 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 type RiskScoreC = t.Type; + +/** + * Types the risk score as: + * - Natural Number (positive integer and not a float), + * - Between the values [0 and 100] inclusive. + */ +export const RiskScore: RiskScoreC = new t.Type( + 'RiskScore', + t.number.is, + (input, context): Either => { + return typeof input === 'number' && Number.isSafeInteger(input) && input >= 0 && input <= 100 + ? t.success(input) + : t.failure(input, context); + }, + t.identity +); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts new file mode 100644 index 0000000000000..342e6f2db2e16 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { UUID } from './uuid'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; +import { left } from 'fp-ts/lib/Either'; + +describe('uuid', () => { + test('it should validate a uuid', () => { + const payload = '4656dc92-5832-11ea-8e2d-0242ac130003'; + const decoded = UUID.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a non uuid', () => { + const payload = '4656dc92-5832-11ea-8e2d'; + const decoded = UUID.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "4656dc92-5832-11ea-8e2d" supplied to ""', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an empty string', () => { + const payload = ''; + const decoded = UUID.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to ""']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.ts new file mode 100644 index 0000000000000..88e9db5964198 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.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 type UUIDC = t.Type; + +const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** + * Types the risk score as: + * - Natural Number (positive integer and not a float), + * - Between the values [0 and 100] inclusive. + */ +export const UUID: UUIDC = new t.Type( + 'UUID', + t.string.is, + (input, context): Either => { + return typeof input === 'string' && regex.test(input) + ? t.success(input) + : t.failure(input, context); + }, + t.identity +); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 36e1a814d8ec2..d19873bbf7018 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { snakeCase } from 'lodash/fp'; +import { has, snakeCase } from 'lodash/fp'; import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../common/constants'; import { LegacyServices } from '../../../types'; @@ -100,17 +100,26 @@ export const createBulkErrorObject = ({ } }; -export interface ImportRuleResponse { - rule_id?: string; - id?: string; - status_code?: number; +export interface ImportRegular { + rule_id: string; + status_code: number; message?: string; - error?: { - status_code: number; - message: string; - }; } +export type ImportRuleResponse = ImportRegular | BulkError; + +export const isBulkError = ( + importRuleResponse: ImportRuleResponse +): importRuleResponse is BulkError => { + return has('error', importRuleResponse); +}; + +export const isImportRegular = ( + importRuleResponse: ImportRuleResponse +): importRuleResponse is ImportRegular => { + return !has('error', importRuleResponse) && has('status_code', importRuleResponse); +}; + export interface ImportSuccessError { success: boolean; success_count: number; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts index 304f9a741c6f4..33f60bf0ba543 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts @@ -21,7 +21,7 @@ describe('getExportAll', () => { const exports = await getExportAll(alertsClient); expect(exports).toEqual({ rulesNdjson: - '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"saved_id":"some-id","timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n', + '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n', exportDetails: '{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n', }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 44d3013263c65..83b487163bdfb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -29,7 +29,7 @@ describe('get_export_by_object_ids', () => { const exports = await getExportByObjectIds(alertsClient, objects); expect(exports).toEqual({ rulesNdjson: - '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"saved_id":"some-id","timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n', + '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n', exportDetails: '{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n', }); }); @@ -92,7 +92,6 @@ describe('get_export_by_object_ids', () => { name: 'Detect Root/Admin Users', query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], - saved_id: 'some-id', timeline_id: 'some-timeline-id', timeline_title: 'some-timeline-title', meta: { someMeta: 'someField' }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index 3d95e9868a1d6..be18b3288f5ab 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -13,7 +13,7 @@ import { SavedObjectsFindResponse, SavedObjectsClientContract, } from 'kibana/server'; -import { AlertsClient } from '../../../../../../../plugins/alerting/server'; +import { AlertsClient, PartialAlert } from '../../../../../../../plugins/alerting/server'; import { Alert } from '../../../../../../../plugins/alerting/common'; import { SIGNALS_ID } from '../../../../common/constants'; import { LegacyRequest } from '../../../types'; @@ -189,12 +189,12 @@ export interface ReadRuleParams { ruleId?: string | undefined | null; } -export const isAlertTypes = (obj: unknown[]): obj is RuleAlertType[] => { - return obj.every(rule => isAlertType(rule)); +export const isAlertTypes = (partialAlert: PartialAlert[]): partialAlert is RuleAlertType[] => { + return partialAlert.every(rule => isAlertType(rule)); }; -export const isAlertType = (obj: unknown): obj is RuleAlertType => { - return get('alertTypeId', obj) === SIGNALS_ID; +export const isAlertType = (partialAlert: PartialAlert): partialAlert is RuleAlertType => { + return partialAlert.alertTypeId === SIGNALS_ID; }; export const isRuleStatusSavedObjectType = ( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index 08cb2e7bc19ee..77eefd3d1d855 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -39,7 +39,7 @@ export interface RuleAlertParams { name: string; query: string | undefined | null; references: string[]; - savedId: string | undefined | null; + savedId?: string | undefined | null; meta: Record | undefined | null; severity: string; tags: string[]; @@ -77,7 +77,7 @@ export type RuleAlertParamsRest = Omit< > & { rule_id: RuleAlertParams['ruleId']; false_positives: RuleAlertParams['falsePositives']; - saved_id: RuleAlertParams['savedId']; + saved_id?: RuleAlertParams['savedId']; timeline_id: RuleAlertParams['timelineId']; timeline_title: RuleAlertParams['timelineTitle']; max_signals: RuleAlertParams['maxSignals']; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts index 82e506b23ca97..abbc8f77e0077 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts @@ -61,7 +61,8 @@ export default ({ getService }: FtrProviderContext): void => { const { body } = await supertest .get(`${DETECTION_ENGINE_RULES_URL}/_find`) .set('kbn-xsrf', 'true') - .send(); + .send() + .expect(200); body.data = [removeServerGeneratedProperties(body.data[0])]; expect(body).to.eql({ @@ -84,7 +85,8 @@ export default ({ getService }: FtrProviderContext): void => { const { body } = await supertest .get(`${DETECTION_ENGINE_RULES_URL}/_find`) .set('kbn-xsrf', 'true') - .send(); + .send() + .expect(200); body.data = [removeServerGeneratedProperties(body.data[0])]; expect(body).to.eql({