diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index 46186479bf726..08e609938938c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -284,7 +284,7 @@ const { patch: threatMatchPatchParams, response: threatMatchResponseParams, } = buildAPISchemas(threatMatchRuleParams); -export { threatMatchCreateParams }; +export { threatMatchCreateParams, threatMatchResponseParams }; const queryRuleParams = { required: { @@ -307,7 +307,7 @@ const { response: queryResponseParams, } = buildAPISchemas(queryRuleParams); -export { queryCreateParams }; +export { queryCreateParams, queryResponseParams }; const savedQueryRuleParams = { required: { @@ -332,7 +332,7 @@ const { response: savedQueryResponseParams, } = buildAPISchemas(savedQueryRuleParams); -export { savedQueryCreateParams }; +export { savedQueryCreateParams, savedQueryResponseParams }; const thresholdRuleParams = { required: { @@ -356,7 +356,7 @@ const { response: thresholdResponseParams, } = buildAPISchemas(thresholdRuleParams); -export { thresholdCreateParams }; +export { thresholdCreateParams, thresholdResponseParams }; const machineLearningRuleParams = { required: { @@ -373,7 +373,7 @@ const { response: machineLearningResponseParams, } = buildAPISchemas(machineLearningRuleParams); -export { machineLearningCreateParams }; +export { machineLearningCreateParams, machineLearningResponseParams }; const newTermsRuleParams = { required: { @@ -397,7 +397,7 @@ const { response: newTermsResponseParams, } = buildAPISchemas(newTermsRuleParams); -export { newTermsCreateParams }; +export { newTermsCreateParams, newTermsResponseParams }; // --------------------------------------- // END type specific parameter definitions @@ -503,14 +503,27 @@ const responseOptionalFields = { execution_summary: RuleExecutionSummary, }; -export const fullResponseSchema = t.intersection([ +const sharedResponseSchema = t.intersection([ baseResponseParams, - responseTypeSpecific, t.exact(t.type(responseRequiredFields)), t.exact(t.partial(responseOptionalFields)), ]); +export type SharedResponseSchema = t.TypeOf; +export const fullResponseSchema = t.intersection([sharedResponseSchema, responseTypeSpecific]); export type FullResponseSchema = t.TypeOf; +// Convenience types for type specific responses +type ResponseSchema = SharedResponseSchema & T; +export type EqlResponseSchema = ResponseSchema>; +export type ThreatMatchResponseSchema = ResponseSchema>; +export type QueryResponseSchema = ResponseSchema>; +export type SavedQueryResponseSchema = ResponseSchema>; +export type ThresholdResponseSchema = ResponseSchema>; +export type MachineLearningResponseSchema = ResponseSchema< + t.TypeOf +>; +export type NewTermsResponseSchema = ResponseSchema>; + export interface RulePreviewLogs { errors: string[]; warnings: string[]; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts index e12fbf2918302..5c934b0d2e040 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts @@ -12,5 +12,3 @@ export * from './import_rules_schema'; export * from './prepackaged_rules_schema'; export * from './prepackaged_rules_status_schema'; export * from './rules_bulk_schema'; -export * from './rules_schema'; -export * from './type_timeline_only_schema'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts index 00800b9474716..69e31522ef40a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts @@ -10,12 +10,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import type { RulesBulkSchema } from './rules_bulk_schema'; import { rulesBulkSchema } from './rules_bulk_schema'; -import type { RulesSchema } from './rules_schema'; import type { ErrorSchema } from './error_schema'; import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getRulesSchemaMock } from './rules_schema.mocks'; import { getErrorSchemaMock } from './error_schema.mocks'; +import type { FullResponseSchema } from '../request'; describe('prepackaged_rule_schema', () => { test('it should validate a regular message and and error together with a uuid', () => { @@ -73,15 +73,14 @@ describe('prepackaged_rule_schema', () => { 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(getPaths(left(message.errors))).toContain( + '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 } = getRulesSchemaMock(); + const rule: FullResponseSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); rule.invalid_extra_data = 'invalid_extra_data'; const payload: RulesBulkSchema = [rule]; const decoded = rulesBulkSchema.decode(payload); @@ -93,7 +92,7 @@ describe('prepackaged_rule_schema', () => { }); 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 } = getRulesSchemaMock(); + const rule: FullResponseSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); rule.invalid_extra_data = 'invalid_extra_data'; const payload: RulesBulkSchema = [getErrorSchemaMock(), rule]; const decoded = rulesBulkSchema.decode(payload); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.ts index 57d812645ed38..65c55f356c44b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.ts @@ -7,8 +7,8 @@ import * as t from 'io-ts'; -import { rulesSchema } from './rules_schema'; +import { fullResponseSchema } from '../request'; import { errorSchema } from './error_schema'; -export const rulesBulkSchema = t.array(t.union([rulesSchema, errorSchema])); +export const rulesBulkSchema = t.array(t.union([fullResponseSchema, errorSchema])); export type RulesBulkSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts index c3fbec8a6d7b3..bf6583a6855f0 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts @@ -6,13 +6,19 @@ */ import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../constants'; +import type { + EqlResponseSchema, + MachineLearningResponseSchema, + QueryResponseSchema, + SavedQueryResponseSchema, + SharedResponseSchema, + ThreatMatchResponseSchema, +} from '../request'; import { getListArrayMock } from '../types/lists.mock'; -import type { RulesSchema } from './rules_schema'; - export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; -export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => ({ +const getResponseBaseParams = (anchorDate: string = ANCHOR_DATE): SharedResponseSchema => ({ author: [], id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', created_at: new Date(anchorDate).toISOString(), @@ -24,45 +30,83 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchem 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', + severity: 'high' as const, severity_mapping: [], updated_by: 'elastic_kibana', tags: ['some fake tag 1', 'some fake tag 2'], to: 'now', - type: 'query', threat: [], version: 1, output_index: '.siem-signals-default', max_signals: 100, risk_score: 55, risk_score_mapping: [], - language: 'kuery', rule_id: 'query-rule-id', interval: '5m', exceptions_list: getListArrayMock(), related_integrations: [], required_fields: [], setup: '', + throttle: 'no_actions', + actions: [], + building_block_type: undefined, + note: undefined, + license: undefined, + outcome: undefined, + alias_target_id: undefined, + alias_purpose: undefined, + timeline_id: undefined, + timeline_title: undefined, + meta: undefined, + rule_name_override: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + namespace: undefined, }); -export const getRulesMlSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => { - const basePayload = getRulesSchemaMock(anchorDate); - const { filters, index, query, language, ...rest } = basePayload; +export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): QueryResponseSchema => ({ + ...getResponseBaseParams(anchorDate), + query: 'user.name: root or user.name: admin', + type: 'query', + language: 'kuery', + index: undefined, + data_view_id: undefined, + filters: undefined, + saved_id: undefined, +}); +export const getSavedQuerySchemaMock = ( + anchorDate: string = ANCHOR_DATE +): SavedQueryResponseSchema => ({ + ...getResponseBaseParams(anchorDate), + query: 'user.name: root or user.name: admin', + type: 'saved_query', + saved_id: 'save id 123', + language: 'kuery', + index: undefined, + data_view_id: undefined, + filters: undefined, +}); +export const getRulesMlSchemaMock = ( + anchorDate: string = ANCHOR_DATE +): MachineLearningResponseSchema => { return { - ...rest, + ...getResponseBaseParams(anchorDate), type: 'machine_learning', anomaly_threshold: 59, machine_learning_job_id: 'some_machine_learning_job_id', }; }; -export const getThreatMatchingSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => { +export const getThreatMatchingSchemaMock = ( + anchorDate: string = ANCHOR_DATE +): ThreatMatchResponseSchema => { return { - ...getRulesSchemaMock(anchorDate), + ...getResponseBaseParams(anchorDate), type: 'threat_match', + query: 'user.name: root or user.name: admin', + language: 'kuery', threat_index: ['index-123'], threat_mapping: [{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }], threat_query: '*:*', @@ -84,6 +128,14 @@ export const getThreatMatchingSchemaMock = (anchorDate: string = ANCHOR_DATE): R }, }, ], + index: undefined, + data_view_id: undefined, + filters: undefined, + saved_id: undefined, + threat_indicator_path: undefined, + threat_language: undefined, + concurrent_searches: undefined, + items_per_search: undefined, }; }; @@ -91,7 +143,9 @@ export const getThreatMatchingSchemaMock = (anchorDate: string = ANCHOR_DATE): R * Useful for e2e backend tests where it doesn't have date time and other * server side properties attached to it. */ -export const getThreatMatchingSchemaPartialMock = (enabled = false): Partial => { +export const getThreatMatchingSchemaPartialMock = ( + enabled = false +): Partial => { return { author: [], created_by: 'elastic', @@ -160,11 +214,17 @@ export const getThreatMatchingSchemaPartialMock = (enabled = false): Partial { +export const getRulesEqlSchemaMock = (anchorDate: string = ANCHOR_DATE): EqlResponseSchema => { return { - ...getRulesSchemaMock(anchorDate), + ...getResponseBaseParams(anchorDate), language: 'eql', type: 'eql', query: 'process where true', + index: undefined, + data_view_id: undefined, + filters: undefined, + timestamp_field: undefined, + event_category_override: undefined, + tiebreaker_field: undefined, }; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts index bac55c8510929..0a337eb28bc1c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts @@ -7,35 +7,23 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import type * as t from 'io-ts'; -import type { RulesSchema } from './rules_schema'; -import { - rulesSchema, - checkTypeDependents, - getDependents, - addSavedId, - addQueryFields, - addTimelineTitle, - addMlFields, - addThreatMatchFields, - addEqlFields, -} from './rules_schema'; import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; -import type { TypeAndTimelineOnly } from './type_timeline_only_schema'; import { getRulesSchemaMock, getRulesMlSchemaMock, + getSavedQuerySchemaMock, getThreatMatchingSchemaMock, getRulesEqlSchemaMock, } from './rules_schema.mocks'; -import type { ListArray } from '@kbn/securitysolution-io-ts-list-types'; +import { fullResponseSchema } from '../request'; +import type { FullResponseSchema } from '../request'; describe('rules_schema', () => { test('it should validate a type of "query" without anything extra', () => { const payload = getRulesSchemaMock(); - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); const expected = getRulesSchemaMock(); @@ -45,10 +33,10 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "query" when it has extra data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); + const payload: FullResponseSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); payload.invalid_extra_data = 'invalid_extra_data'; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -57,55 +45,48 @@ describe('rules_schema', () => { }); test('it should NOT validate invalid_data for the type', () => { - const payload: Omit & { type: string } = getRulesSchemaMock(); + const payload: Omit & { type: string } = getRulesSchemaMock(); payload.type = 'invalid_data'; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.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(getPaths(left(message.errors))).toHaveLength(1); expect(message.schema).toEqual({}); }); - test('it should NOT validate a type of "query" with a saved_id together', () => { - const payload = getRulesSchemaMock(); + test('it should validate a type of "query" with a saved_id together', () => { + const payload: FullResponseSchema & { saved_id?: string } = getRulesSchemaMock(); payload.type = 'query'; payload.saved_id = 'save id 123'; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.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({}); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); }); test('it should validate a type of "saved_query" with a "saved_id" dependent', () => { - const payload = getRulesSchemaMock(); - payload.type = 'saved_query'; - payload.saved_id = 'save id 123'; + const payload = getSavedQuerySchemaMock(); - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getRulesSchemaMock(); - - expected.type = 'saved_query'; - expected.saved_id = 'save id 123'; + const expected = getSavedQuerySchemaMock(); 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 = getRulesSchemaMock(); - payload.type = 'saved_query'; + const payload: FullResponseSchema & { saved_id?: string } = getSavedQuerySchemaMock(); + // @ts-expect-error delete payload.saved_id; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -116,12 +97,11 @@ describe('rules_schema', () => { }); test('it should NOT validate a type of "saved_query" when it has extra data', () => { - const payload: RulesSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); - payload.type = 'saved_query'; - payload.saved_id = 'save id 123'; + const payload: FullResponseSchema & { saved_id?: string; invalid_extra_data?: string } = + getSavedQuerySchemaMock(); payload.invalid_extra_data = 'invalid_extra_data'; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -134,7 +114,7 @@ describe('rules_schema', () => { payload.timeline_id = 'some timeline id'; payload.timeline_title = 'some timeline title'; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); const expected = getRulesSchemaMock(); @@ -146,12 +126,12 @@ describe('rules_schema', () => { }); 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 } = getRulesSchemaMock(); + const payload: FullResponseSchema & { invalid_extra_data?: string } = getRulesSchemaMock(); 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 decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -159,575 +139,11 @@ describe('rules_schema', () => { expect(message.schema).toEqual({}); }); - test('it should NOT validate a type of "timeline_id" if there is NOT a "timeline_title" dependent', () => { - const payload = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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({}); - }); - - describe('checkTypeDependents', () => { - test('it should validate a type of "query" without anything extra', () => { - const payload = getRulesSchemaMock(); - - const decoded = checkTypeDependents(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getRulesSchemaMock(); - - 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 } = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - - 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 = getRulesSchemaMock(); - 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 } = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 } = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getRulesSchemaMock(); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it should validate a namespace as string', () => { - const payload = { - ...getRulesSchemaMock(), - namespace: 'a namespace', - }; - 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([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate invalid_data for the type', () => { - const payload: Omit & { type: string } = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - - 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 = getRulesSchemaMock(); - 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 } = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 } = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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 = getRulesSchemaMock(); - 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({}); - }); - - test('it validates an ML rule response', () => { - const payload = getRulesMlSchemaMock(); - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getRulesMlSchemaMock(); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it rejects a response with both ML and query properties', () => { - const payload = { - ...getRulesSchemaMock(), - ...getRulesMlSchemaMock(), - }; - - 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 "query,language"']); - expect(message.schema).toEqual({}); - }); - - test('it validates a threat_match response', () => { - const payload = getThreatMatchingSchemaMock(); - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getThreatMatchingSchemaMock(); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - - test('it rejects a response with threat_match properties but type of "query"', () => { - const payload: RulesSchema = { - ...getThreatMatchingSchemaMock(), - type: 'query', - }; - - 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 "threat_index,["index-123"],threat_mapping,[{"entries":[{"field":"host.name","type":"mapping","value":"host.name"}]}],threat_query,threat_filters,[{"bool":{"must":[{"query_string":{"query":"host.name: linux","analyze_wildcard":true,"time_zone":"Zulu"}}],"filter":[],"should":[],"must_not":[]}}]"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it validates an eql rule response', () => { - const payload = getRulesEqlSchemaMock(); - - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - const expected = getRulesEqlSchemaMock(); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(expected); - }); - }); - - 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 2 given a "saved_query"', () => { - const array = addSavedId({ type: 'saved_query' }); - expect(array.length).toEqual(2); - }); - }); - - 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); - }); - }); - - describe('addQueryFields', () => { - test('should return empty array if type is not "query"', () => { - const fields = addQueryFields({ type: 'machine_learning' }); - const expected: t.Mixed[] = []; - expect(fields).toEqual(expected); - }); - - test('should return two fields for a rule of type "query"', () => { - const fields = addQueryFields({ type: 'query' }); - expect(fields.length).toEqual(3); - }); - - test('should return two fields for a rule of type "threshold"', () => { - const fields = addQueryFields({ type: 'threshold' }); - expect(fields.length).toEqual(3); - }); - - test('should return two fields for a rule of type "saved_query"', () => { - const fields = addQueryFields({ type: 'saved_query' }); - expect(fields.length).toEqual(3); - }); - - test('should return two fields for a rule of type "threat_match"', () => { - const fields = addQueryFields({ type: 'threat_match' }); - expect(fields.length).toEqual(3); - }); - }); - - describe('addMlFields', () => { - test('should return empty array if type is not "machine_learning"', () => { - const fields = addMlFields({ type: 'query' }); - const expected: t.Mixed[] = []; - expect(fields).toEqual(expected); - }); - - test('should return two fields for a rule of type "machine_learning"', () => { - const fields = addMlFields({ type: 'machine_learning' }); - expect(fields.length).toEqual(2); - }); - }); - describe('exceptions_list', () => { test('it should validate an empty array for "exceptions_list"', () => { const payload = getRulesSchemaMock(); payload.exceptions_list = []; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); const expected = getRulesSchemaMock(); @@ -737,11 +153,11 @@ describe('rules_schema', () => { }); test('it should NOT validate when "exceptions_list" is not expected type', () => { - const payload: Omit & { + const payload: Omit & { exceptions_list?: string; } = { ...getRulesSchemaMock(), exceptions_list: 'invalid_data' }; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); @@ -750,53 +166,13 @@ describe('rules_schema', () => { ]); expect(message.schema).toEqual({}); }); - - test('it should default to empty array if "exceptions_list" is undefined ', () => { - const payload: Omit & { - exceptions_list?: ListArray; - } = getRulesSchemaMock(); - payload.exceptions_list = undefined; - - const decoded = rulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ ...payload, exceptions_list: [] }); - }); - }); - - describe('addThreatMatchFields', () => { - test('should return empty array if type is not "threat_match"', () => { - const fields = addThreatMatchFields({ type: 'query' }); - const expected: t.Mixed[] = []; - expect(fields).toEqual(expected); - }); - - test('should return nine (9) fields for a rule of type "threat_match"', () => { - const fields = addThreatMatchFields({ type: 'threat_match' }); - expect(fields.length).toEqual(10); - }); - }); - - describe('addEqlFields', () => { - test('should return empty array if type is not "eql"', () => { - const fields = addEqlFields({ type: 'query' }); - const expected: t.Mixed[] = []; - expect(fields).toEqual(expected); - }); - - test('should return 3 fields for a rule of type "eql"', () => { - const fields = addEqlFields({ type: 'eql' }); - expect(fields.length).toEqual(6); - }); }); describe('data_view_id', () => { test('it should validate a type of "query" with "data_view_id" defined', () => { const payload = { ...getRulesSchemaMock(), data_view_id: 'logs-*' }; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); const expected = { ...getRulesSchemaMock(), data_view_id: 'logs-*' }; @@ -806,18 +182,16 @@ describe('rules_schema', () => { }); test('it should validate a type of "saved_query" with "data_view_id" defined', () => { - const payload = getRulesSchemaMock(); - payload.type = 'saved_query'; - payload.saved_id = 'save id 123'; + const payload: FullResponseSchema & { saved_id?: string; data_view_id?: string } = + getSavedQuerySchemaMock(); payload.data_view_id = 'logs-*'; - const decoded = rulesSchema.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); - const expected = getRulesSchemaMock(); + const expected: FullResponseSchema & { saved_id?: string; data_view_id?: string } = + getSavedQuerySchemaMock(); - expected.type = 'saved_query'; - expected.saved_id = 'save id 123'; expected.data_view_id = 'logs-*'; expect(getPaths(left(message.errors))).toEqual([]); @@ -827,8 +201,7 @@ describe('rules_schema', () => { test('it should validate a type of "eql" with "data_view_id" defined', () => { const payload = { ...getRulesEqlSchemaMock(), data_view_id: 'logs-*' }; - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); const expected = { ...getRulesEqlSchemaMock(), data_view_id: 'logs-*' }; @@ -840,8 +213,7 @@ describe('rules_schema', () => { test('it should validate a type of "threat_match" with "data_view_id" defined', () => { const payload = { ...getThreatMatchingSchemaMock(), data_view_id: 'logs-*' }; - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); const expected = { ...getThreatMatchingSchemaMock(), data_view_id: 'logs-*' }; @@ -853,8 +225,7 @@ describe('rules_schema', () => { test('it should NOT validate a type of "machine_learning" with "data_view_id" defined', () => { const payload = { ...getRulesMlSchemaMock(), data_view_id: 'logs-*' }; - const dependents = getDependents(payload); - const decoded = dependents.decode(payload); + const decoded = fullResponseSchema.decode(payload); const checked = exactCheck(payload, decoded); const message = pipe(checked, foldLeftRight); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts deleted file mode 100644 index 794ef71bf0536..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { isObject } from 'lodash/fp'; -import type { Either } from 'fp-ts/lib/Either'; -import { left, fold } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { - actions, - from, - machine_learning_job_id, - risk_score, - DefaultRiskScoreMappingArray, - DefaultSeverityMappingArray, - threat_index, - concurrent_searches, - items_per_search, - threat_query, - threat_filters, - threat_mapping, - threat_language, - threat_indicator_path, - threats, - type, - language, - severity, - throttle, - max_signals, -} from '@kbn/securitysolution-io-ts-alerting-types'; -import { DefaultStringArray, version } from '@kbn/securitysolution-io-ts-types'; -import { DefaultListArray } from '@kbn/securitysolution-io-ts-list-types'; - -import { isMlRule } from '../../../machine_learning/helpers'; -import { isThresholdRule } from '../../utils'; -import { RuleExecutionSummary } from '../../rule_monitoring'; -import { - anomaly_threshold, - data_view_id, - description, - enabled, - timestamp_field, - event_category_override, - tiebreaker_field, - false_positives, - id, - immutable, - index, - interval, - rule_id, - name, - output_index, - query, - references, - updated_by, - tags, - to, - created_at, - created_by, - updated_at, - saved_id, - timeline_id, - timeline_title, - threshold, - filters, - meta, - outcome, - alias_target_id, - alias_purpose, - note, - building_block_type, - license, - rule_name_override, - timestamp_override, - namespace, - RelatedIntegrationArray, - RequiredFieldArray, - SetupGuide, -} from '../common'; - -import type { TypeAndTimelineOnly } from './type_timeline_only_schema'; -import { typeAndTimelineOnlySchema } from './type_timeline_only_schema'; - -/** - * 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({ - author: DefaultStringArray, - description, - enabled, - false_positives, - from, - id, - immutable, - interval, - rule_id, - output_index, - max_signals, - risk_score, - risk_score_mapping: DefaultRiskScoreMappingArray, - name, - references, - severity, - severity_mapping: DefaultSeverityMappingArray, - updated_by, - tags, - to, - type, - threat: threats, - created_at, - updated_at, - created_by, - version, - exceptions_list: DefaultListArray, - related_integrations: RelatedIntegrationArray, - required_fields: RequiredFieldArray, - setup: SetupGuide, -}); - -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({ - // All but ML - data_view_id, - - // query fields - language, - query, - - // eql only fields - timestamp_field, - event_category_override, - tiebreaker_field, - - // when type = saved_query, saved_id is required - saved_id, - - // These two are required together or not at all. - timeline_id, - timeline_title, - - // ML fields - anomaly_threshold, - machine_learning_job_id, - - // Threshold fields - threshold, - - // Threat Match fields - threat_filters, - threat_index, - threat_query, - concurrent_searches, - items_per_search, - threat_mapping, - threat_language, - threat_indicator_path, -}); - -/** - * 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({ - actions, - building_block_type, - license, - throttle, - rule_name_override, - timestamp_override, - filters, - meta, - outcome, - alias_target_id, - alias_purpose, - index, - namespace, - note, - uuid: id, // Move to 'required' post-migration - execution_summary: RuleExecutionSummary, -}); - -/** - * 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; - -export const addSavedId = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.type === 'saved_query') { - return [ - t.exact(t.type({ saved_id: dependentRulesSchema.props.saved_id })), - t.exact(t.partial({ data_view_id: dependentRulesSchema.props.data_view_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 addQueryFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (['query', 'saved_query', 'threshold', 'threat_match'].includes(typeAndTimelineOnly.type)) { - return [ - t.exact(t.type({ query: dependentRulesSchema.props.query })), - t.exact(t.type({ language: dependentRulesSchema.props.language })), - t.exact(t.partial({ data_view_id: dependentRulesSchema.props.data_view_id })), - ]; - } else { - return []; - } -}; - -export const addMlFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (isMlRule(typeAndTimelineOnly.type)) { - return [ - t.exact(t.type({ anomaly_threshold: dependentRulesSchema.props.anomaly_threshold })), - t.exact( - t.type({ machine_learning_job_id: dependentRulesSchema.props.machine_learning_job_id }) - ), - ]; - } else { - return []; - } -}; - -export const addThresholdFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (isThresholdRule(typeAndTimelineOnly.type)) { - return [ - t.exact(t.type({ threshold: dependentRulesSchema.props.threshold })), - t.exact(t.partial({ saved_id: dependentRulesSchema.props.saved_id })), - t.exact(t.partial({ data_view_id: dependentRulesSchema.props.data_view_id })), - ]; - } else { - return []; - } -}; - -export const addEqlFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.type === 'eql') { - return [ - t.exact(t.partial({ timestamp_field: dependentRulesSchema.props.timestamp_field })), - t.exact( - t.partial({ event_category_override: dependentRulesSchema.props.event_category_override }) - ), - t.exact(t.partial({ tiebreaker_field: dependentRulesSchema.props.tiebreaker_field })), - t.exact(t.type({ query: dependentRulesSchema.props.query })), - t.exact(t.type({ language: dependentRulesSchema.props.language })), - t.exact(t.partial({ data_view_id: dependentRulesSchema.props.data_view_id })), - ]; - } else { - return []; - } -}; - -export const addThreatMatchFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.type === 'threat_match') { - return [ - t.exact(t.partial({ data_view_id: dependentRulesSchema.props.data_view_id })), - t.exact(t.type({ threat_query: dependentRulesSchema.props.threat_query })), - t.exact(t.type({ threat_index: dependentRulesSchema.props.threat_index })), - t.exact(t.type({ threat_mapping: dependentRulesSchema.props.threat_mapping })), - t.exact(t.partial({ threat_language: dependentRulesSchema.props.threat_language })), - t.exact(t.partial({ threat_filters: dependentRulesSchema.props.threat_filters })), - t.exact( - t.partial({ threat_indicator_path: dependentRulesSchema.props.threat_indicator_path }) - ), - t.exact(t.partial({ saved_id: dependentRulesSchema.props.saved_id })), - t.exact(t.partial({ concurrent_searches: dependentRulesSchema.props.concurrent_searches })), - t.exact( - t.partial({ - items_per_search: dependentRulesSchema.props.items_per_search, - }) - ), - ]; - } else { - return []; - } -}; - -export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed => { - const dependents: t.Mixed[] = [ - t.exact(requiredRulesSchema), - t.exact(partialRulesSchema), - ...addSavedId(typeAndTimelineOnly), - ...addTimelineTitle(typeAndTimelineOnly), - ...addQueryFields(typeAndTimelineOnly), - ...addMlFields(typeAndTimelineOnly), - ...addThresholdFields(typeAndTimelineOnly), - ...addEqlFields(typeAndTimelineOnly), - ...addThreatMatchFields(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/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts deleted file mode 100644 index 8026d99713214..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { left } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import type { TypeAndTimelineOnly } from './type_timeline_only_schema'; -import { typeAndTimelineOnlySchema } from './type_timeline_only_schema'; -import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; - -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/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.ts deleted file mode 100644 index b164ab9b44e4f..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { type } from '@kbn/securitysolution-io-ts-alerting-types'; -import { timeline_id } from '../common/schemas'; - -/** - * 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/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 0a5d5170473fb..6b0051f12bc29 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -5,11 +5,11 @@ * 2.0. */ -import type { RulesSchema } from '../../common/detection_engine/schemas/response'; import { rawRules } from '../../server/lib/detection_engine/rules/prepackaged_rules'; import { getMockThreatData } from '../../public/detections/mitre/mitre_tactics_techniques'; import type { CompleteTimeline } from './timeline'; import { getTimeline, getIndicatorMatchTimelineTemplate } from './timeline'; +import type { FullResponseSchema } from '../../common/detection_engine/schemas/request'; export const totalNumberOfPrebuiltRules = rawRules.length; @@ -488,7 +488,9 @@ export const getEditedRule = (): CustomRule => ({ tags: [...getExistingRule().tags, 'edited'], }); -export const expectedExportedRule = (ruleResponse: Cypress.Response): string => { +export const expectedExportedRule = ( + ruleResponse: Cypress.Response +): string => { const { id, updated_at: updatedAt, @@ -498,14 +500,20 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response name, risk_score: riskScore, severity, - query, tags, timeline_id: timelineId, timeline_title: timelineTitle, } = ruleResponse.body; + let query: string | undefined; + if (ruleResponse.body.type === 'query') { + query = ruleResponse.body.query; + } + // NOTE: Order of the properties in this object matters for the tests to work. - const rule: RulesSchema = { + // TODO: Follow up https://github.com/elastic/kibana/pull/137628 and add an explicit type to this object + // without using Partial + const rule: Partial = { id, updated_at: updatedAt, updated_by: updatedBy, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts index 69aa2a4502bc0..e445e5b935af2 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts @@ -5,10 +5,8 @@ * 2.0. */ -import type { - GetInstalledIntegrationsResponse, - RulesSchema, -} from '../../../../../../common/detection_engine/schemas/response'; +import type { FullResponseSchema } from '../../../../../../common/detection_engine/schemas/request'; +import type { GetInstalledIntegrationsResponse } from '../../../../../../common/detection_engine/schemas/response'; import { getRulesSchemaMock } from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { savedRuleMock, rulesMock } from '../mock'; @@ -25,14 +23,16 @@ import type { FetchRulesProps, } from '../types'; -export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise => +export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise => Promise.resolve(getRulesSchemaMock()); -export const createRule = async ({ rule, signal }: CreateRulesProps): Promise => +export const createRule = async ({ rule, signal }: CreateRulesProps): Promise => Promise.resolve(getRulesSchemaMock()); -export const patchRule = async ({ ruleProperties, signal }: PatchRuleProps): Promise => - Promise.resolve(getRulesSchemaMock()); +export const patchRule = async ({ + ruleProperties, + signal, +}: PatchRuleProps): Promise => Promise.resolve(getRulesSchemaMock()); export const getPrePackagedRulesStatus = async ({ signal, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index f2e78eeee99ef..63754adc5e7c8 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -25,7 +25,6 @@ import type { PreviewResponse, } from '../../../../../common/detection_engine/schemas/request'; import type { - RulesSchema, GetInstalledIntegrationsResponse, RulesReferencedByExceptionListsSchema, } from '../../../../../common/detection_engine/schemas/response'; @@ -74,8 +73,8 @@ export const createRule = async ({ rule, signal }: CreateRulesProps): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { +export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { method: 'PUT', body: JSON.stringify(rule), signal, @@ -92,8 +91,11 @@ export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { +export const patchRule = async ({ + ruleProperties, + signal, +}: PatchRuleProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { method: 'PATCH', body: JSON.stringify(ruleProperties), signal, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index ccd0eb5c80fe6..f949e927ec095 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -8,9 +8,9 @@ import { Readable } from 'stream'; import type { HapiReadableStream } from '../../rules/types'; -import type { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; +import type { FullResponseSchema } from '../../../../../common/detection_engine/schemas/request'; /** * Given a string, builds a hapi stream as our @@ -34,10 +34,7 @@ export const buildHapiStream = (string: string, filename = 'file.ndjson'): HapiR return stream; }; -export const getOutputRuleAlertForRest = (): Omit< - RulesSchema, - 'machine_learning_job_id' | 'anomaly_threshold' -> => ({ +export const getOutputRuleAlertForRest = (): FullResponseSchema => ({ author: ['Elastic'], actions: [], building_block_type: 'default', @@ -93,4 +90,11 @@ export const getOutputRuleAlertForRest = (): Omit< related_integrations: [], required_fields: [], setup: '', + outcome: undefined, + alias_target_id: undefined, + alias_purpose: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + namespace: undefined, + data_view_id: undefined, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index c5009138b4078..6e76de5aa420d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -16,7 +16,7 @@ import { readRules } from '../../rules/read_rules'; import { buildSiemResponse } from '../utils'; import { createRulesSchema } from '../../../../../common/detection_engine/schemas/request'; -import { newTransformValidate } from './validate'; +import { transformValidate } from './validate'; import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents'; import { createRules } from '../../rules/create_rules'; import { checkDefaultRuleExceptionListReferences } from './utils/check_for_default_rule_exception_list'; @@ -89,7 +89,7 @@ export const createRulesRoute = ( const ruleExecutionSummary = await ruleExecutionLog.getExecutionSummary(createdRule.id); - const [validated, errors] = newTransformValidate(createdRule, ruleExecutionSummary); + const [validated, errors] = transformValidate(createdRule, ruleExecutionSummary); if (errors != null) { return siemResponse.error({ statusCode: 500, body: errors }); } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index 2df4cb712ddd2..9dfd5b1efed7c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -14,7 +14,6 @@ import pMap from 'p-map'; import type { PartialRule, FindResult } from '@kbn/alerting-plugin/server'; import type { ActionsClient, FindActionResult } from '@kbn/actions-plugin/server'; import type { RuleExecutionSummary } from '../../../../../common/detection_engine/rule_monitoring'; -import type { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import type { ImportRulesSchema } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; import type { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import type { RuleAlertType } from '../../rules/types'; @@ -26,6 +25,7 @@ import type { RuleParams } from '../../schemas/rule_schemas'; // eslint-disable-next-line no-restricted-imports import type { LegacyRulesActionsSavedObject } from '../../rule_actions/legacy_get_rule_actions_saved_object'; import type { RuleExecutionSummariesByRuleId } from '../../rule_monitoring'; +import type { FullResponseSchema } from '../../../../../common/detection_engine/schemas/request'; type PromiseFromStreams = ImportRulesSchema | Error; const MAX_CONCURRENT_SEARCHES = 10; @@ -92,7 +92,7 @@ export const getIdBulkError = ({ export const transformAlertsToRules = ( rules: RuleAlertType[], legacyRuleActions: Record -): Array> => { +): FullResponseSchema[] => { return rules.map((rule) => internalRuleToAPIResponse(rule, null, legacyRuleActions[rule.id])); }; @@ -104,7 +104,7 @@ export const transformFindAlerts = ( page: number; perPage: number; total: number; - data: Array>; + data: Array>; } | null => { return { page: ruleFindResults.page, @@ -121,7 +121,7 @@ export const transform = ( rule: PartialRule, ruleExecutionSummary?: RuleExecutionSummary | null, legacyRuleActions?: LegacyRulesActionsSavedObject | null -): Partial | null => { +): FullResponseSchema | null => { if (isAlertType(rule)) { return internalRuleToAPIResponse(rule, ruleExecutionSummary, legacyRuleActions); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts index 21db7e52e4f8d..84f693a529a68 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts @@ -7,14 +7,14 @@ import { transformValidate, transformValidateBulkError } from './validate'; import type { BulkError } from '../utils'; -import type { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; import { getRuleMock } from '../__mocks__/request_responses'; import { ruleExecutionSummaryMock } from '../../../../../common/detection_engine/rule_monitoring/mocks'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; +import type { FullResponseSchema } from '../../../../../common/detection_engine/schemas/request'; -export const ruleOutput = (): RulesSchema => ({ +export const ruleOutput = (): FullResponseSchema => ({ actions: [], author: ['Elastic'], building_block_type: 'default', @@ -67,6 +67,15 @@ export const ruleOutput = (): RulesSchema => ({ related_integrations: [], required_fields: [], setup: '', + outcome: undefined, + alias_target_id: undefined, + alias_purpose: undefined, + rule_name_override: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + namespace: undefined, + data_view_id: undefined, + saved_id: undefined, }); describe('validate', () => { @@ -114,7 +123,7 @@ describe('validate', () => { const rule = getRuleMock(getQueryRuleParams()); const ruleExecutionSumary = ruleExecutionSummaryMock.getSummarySucceeded(); const validatedOrError = transformValidateBulkError('rule-1', rule, ruleExecutionSumary); - const expected: RulesSchema = { + const expected: FullResponseSchema = { ...ruleOutput(), execution_summary: ruleExecutionSumary, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts index 4183f217a61fe..42e50db79294a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts @@ -11,8 +11,6 @@ import type { PartialRule } from '@kbn/alerting-plugin/server'; import type { RuleExecutionSummary } from '../../../../../common/detection_engine/rule_monitoring'; import type { FullResponseSchema } from '../../../../../common/detection_engine/schemas/request'; import { fullResponseSchema } from '../../../../../common/detection_engine/schemas/request'; -import type { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; -import { rulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import { isAlertType } from '../../rules/types'; import type { BulkError } from '../utils'; import { createBulkErrorObject } from '../utils'; @@ -26,19 +24,6 @@ export const transformValidate = ( rule: PartialRule, ruleExecutionSummary: RuleExecutionSummary | null, legacyRuleActions?: LegacyRulesActionsSavedObject | null -): [RulesSchema | null, string | null] => { - const transformed = transform(rule, ruleExecutionSummary, legacyRuleActions); - if (transformed == null) { - return [null, 'Internal error transforming']; - } else { - return validateNonExact(transformed, rulesSchema); - } -}; - -export const newTransformValidate = ( - rule: PartialRule, - ruleExecutionSummary: RuleExecutionSummary | null, - legacyRuleActions?: LegacyRulesActionsSavedObject | null ): [FullResponseSchema | null, string | null] => { const transformed = transform(rule, ruleExecutionSummary, legacyRuleActions); if (transformed == null) { @@ -52,10 +37,10 @@ export const transformValidateBulkError = ( ruleId: string, rule: PartialRule, ruleExecutionSummary: RuleExecutionSummary | null -): RulesSchema | BulkError => { +): FullResponseSchema | BulkError => { if (isAlertType(rule)) { const transformed = internalRuleToAPIResponse(rule, ruleExecutionSummary); - const [validated, errors] = validateNonExact(transformed, rulesSchema); + const [validated, errors] = validateNonExact(transformed, fullResponseSchema); if (errors != null || validated == null) { return createBulkErrorObject({ ruleId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 31bdfb398c18a..4fea86a2e3395 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -213,6 +213,13 @@ describe('get_export_by_object_ids', () => { version: 1, exceptions_list: getListArrayMock(), execution_summary: undefined, + outcome: undefined, + alias_target_id: undefined, + alias_purpose: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + namespace: undefined, + data_view_id: undefined, }, ], }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts index d5008c87f3b6d..e044c8fdfd1ca 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -11,7 +11,6 @@ import { transformDataToNdjson } from '@kbn/securitysolution-utils'; import type { Logger } from '@kbn/core/server'; import type { ExceptionListClient } from '@kbn/lists-plugin/server'; import type { RulesClient, RuleExecutorServices } from '@kbn/alerting-plugin/server'; -import type { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; @@ -22,10 +21,11 @@ import { getRuleExceptionsForExport } from './get_export_rule_exceptions'; // eslint-disable-next-line no-restricted-imports import { legacyGetBulkRuleActionsSavedObject } from '../rule_actions/legacy_get_bulk_rule_actions_saved_object'; import { internalRuleToAPIResponse } from '../schemas/rule_converters'; +import type { FullResponseSchema } from '../../../../common/detection_engine/schemas/request'; interface ExportSuccessRule { statusCode: 200; - rule: Partial; + rule: FullResponseSchema; } interface ExportFailedRule { @@ -36,7 +36,7 @@ interface ExportFailedRule { export interface RulesErrors { exportedCount: number; missingRules: Array<{ rule_id: string }>; - rules: Array>; + rules: FullResponseSchema[]; } export const getExportByObjectIds = async ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts index 30be45f5eb163..204d78f5fe7d2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts @@ -6,12 +6,12 @@ */ import type { ExportExceptionDetails } from '@kbn/securitysolution-io-ts-list-types'; +import type { FullResponseSchema } from '../../../../common/detection_engine/schemas/request'; import type { ExportRulesDetails } from '../../../../common/detection_engine/schemas/response/export_rules_details_schema'; -import type { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; export const getExportDetailsNdjson = ( - rules: Array>, + rules: FullResponseSchema[], missingRules: Array<{ rule_id: string }> = [], exceptionDetails?: ExportExceptionDetails ): string => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 6c7d5d581ce61..d3cdcd82b4989 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -497,6 +497,25 @@ export const sampleSignalHit = (): SignalHit => ({ related_integrations: [], required_fields: [], setup: '', + throttle: 'no_actions', + actions: [], + building_block_type: undefined, + note: undefined, + license: undefined, + outcome: undefined, + alias_target_id: undefined, + alias_purpose: undefined, + timeline_id: undefined, + timeline_title: undefined, + meta: undefined, + rule_name_override: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + namespace: undefined, + index: undefined, + data_view_id: undefined, + filters: undefined, + saved_id: undefined, }, depth: 1, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index db88284bc8881..5609eed4c0801 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -19,7 +19,6 @@ import type { ListClient } from '@kbn/lists-plugin/server'; import type { EcsFieldMap } from '@kbn/rule-registry-plugin/common/assets/field_maps/ecs_field_map'; import type { TypeOfFieldMap } from '@kbn/rule-registry-plugin/common/field_map'; import type { Status } from '../../../../common/detection_engine/schemas/common/schemas'; -import type { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import type { BaseHit, RuleAlertAction, @@ -42,6 +41,7 @@ import type { WrappedFieldsLatest, } from '../../../../common/detection_engine/schemas/alerts'; import type { IRuleExecutionLogForExecutors } from '../rule_monitoring'; +import type { FullResponseSchema } from '../../../../common/detection_engine/schemas/request'; export interface ThresholdResult { terms?: Array<{ @@ -192,7 +192,7 @@ export interface Signal { _meta?: { version: number; }; - rule: RulesSchema; + rule: FullResponseSchema; /** * @deprecated Use "parents" instead of "parent" */ diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts index 4d4bda5e6b4e0..c7e49ac77e7be 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts @@ -18,7 +18,7 @@ import { BulkAction, BulkActionEditType, } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request/perform_bulk_action_schema'; -import { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response'; +import type { FullResponseSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { binaryToString, @@ -375,7 +375,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(rulesResponse.total).to.eql(2); - rulesResponse.data.forEach((rule: RulesSchema) => { + rulesResponse.data.forEach((rule: FullResponseSchema) => { expect(rule.actions).to.eql([ { action_type_id: '.slack', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts index 49c1ba045d5e6..1b9dcb5da28fd 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts @@ -82,6 +82,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); const outputRule = getSimpleMlRuleOutput(); + // @ts-expect-error type narrowing is lost due to Omit<> outputRule.machine_learning_job_id = ['legacy_job_id']; outputRule.version = 2; const bodyToCompare = removeServerGeneratedProperties(body); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts index 19447dec2b4a8..dc7209b9f1c98 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts @@ -19,8 +19,6 @@ import { deleteSignalsIndex, getSimpleRuleOutput, removeServerGeneratedProperties, - getSimpleRuleOutputWithoutRuleId, - removeServerGeneratedPropertiesIncludingRuleId, getSimpleRuleUpdate, createRule, getSimpleRule, @@ -282,16 +280,16 @@ export default ({ getService }: FtrProviderContext) => { .send([updatedRule1, updatedRule2]) .expect(200); - const outputRule1 = getSimpleRuleOutputWithoutRuleId('rule-1'); + const outputRule1 = getSimpleRuleOutput('rule-1'); outputRule1.name = 'some other name'; outputRule1.version = 2; - const outputRule2 = getSimpleRuleOutputWithoutRuleId('rule-2'); + const outputRule2 = getSimpleRuleOutput('rule-2'); outputRule2.name = 'some other name'; outputRule2.version = 2; - const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]); - const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]); + const bodyToCompare1 = removeServerGeneratedProperties(body[0]); + const bodyToCompare2 = removeServerGeneratedProperties(body[1]); expect(bodyToCompare1).to.eql(outputRule1); expect(bodyToCompare2).to.eql(outputRule2); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts index f44e72f5cd50a..647c4dddb2bb1 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import type { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; -import { +import type { CreateRulesSchema, EqlCreateSchema, QueryCreateSchema, @@ -18,7 +18,6 @@ import { ThresholdCreateSchema, } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; import { getCreateExceptionListItemMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock'; -import { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response'; import { getCreateExceptionListMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; @@ -106,7 +105,7 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, log, ruleWithException); - const expected: Partial = { + const expected = { ...getSimpleRuleOutput(), exceptions_list: [ { @@ -147,7 +146,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccessOrStatus(supertest, log, rule.id); const bodyToCompare = removeServerGeneratedProperties(rule); - const expected: Partial = { + const expected = { ...getSimpleRuleOutput(), enabled: true, exceptions_list: [ diff --git a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule.ts b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule.ts index 1db5c784660fc..4f5cfdcd3ba56 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule.ts @@ -5,13 +5,13 @@ * 2.0. */ -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; +import type { CreateRulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; /** * This will return a complex rule with all the outputs possible * @param ruleId The ruleId to set which is optional and defaults to rule-1 */ -export const getComplexRule = (ruleId = 'rule-1'): Partial => ({ +export const getComplexRule = (ruleId = 'rule-1'): CreateRulesSchema => ({ actions: [], author: [], name: 'Complex Rule Query', @@ -92,4 +92,6 @@ export const getComplexRule = (ruleId = 'rule-1'): Partial => ({ note: '# some investigation documentation', version: 1, query: 'user.name: root or user.name: admin', + throttle: 'no_actions', + exceptions_list: [], }); diff --git a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts index cc33c2ebff447..1491829b33999 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts @@ -5,13 +5,15 @@ * 2.0. */ -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; +import type { FullResponseSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; +// TODO: Follow up https://github.com/elastic/kibana/pull/137628 and add an explicit type to this object +// without using Partial /** * This will return a complex rule with all the outputs possible * @param ruleId The ruleId to set which is optional and defaults to rule-1 */ -export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial => ({ +export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial => ({ actions: [], author: [], created_by: 'elastic', diff --git a/x-pack/test/detection_engine_api_integration/utils/get_rule.ts b/x-pack/test/detection_engine_api_integration/utils/get_rule.ts index da28e867bc976..b1036e1f8b682 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_rule.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_rule.ts @@ -7,7 +7,7 @@ import type { ToolingLog } from '@kbn/tooling-log'; import type SuperTest from 'supertest'; -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; +import type { FullResponseSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; @@ -21,7 +21,7 @@ export const getRule = async ( supertest: SuperTest.SuperTest, log: ToolingLog, ruleId: string -): Promise => { +): Promise => { const response = await supertest .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=${ruleId}`) .set('kbn-xsrf', 'true'); diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_ml_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_ml_rule_output.ts index c845c0d343261..56afa355b0482 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_ml_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_ml_rule_output.ts @@ -5,15 +5,13 @@ * 2.0. */ -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; -import { getSimpleRuleOutput } from './get_simple_rule_output'; - -export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial => { - const rule = getSimpleRuleOutput(ruleId); - const { query, language, index, ...rest } = rule; +import type { MachineLearningResponseSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; +import { getMockSharedResponseSchema } from './get_simple_rule_output'; +import { removeServerGeneratedProperties } from './remove_server_generated_properties'; +const getBaseMlRuleOutput = (ruleId = 'rule-1'): MachineLearningResponseSchema => { return { - ...rest, + ...getMockSharedResponseSchema(ruleId), name: 'Simple ML Rule', description: 'Simple Machine Learning Rule', anomaly_threshold: 44, @@ -21,3 +19,7 @@ export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial = type: 'machine_learning', }; }; + +export const getSimpleMlRuleOutput = (ruleId = 'rule-1') => { + return removeServerGeneratedProperties(getBaseMlRuleOutput(ruleId)); +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts index 0d6cf9905d4a2..1fe2f2adecc79 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts @@ -5,13 +5,16 @@ * 2.0. */ -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; +import type { + FullResponseSchema, + SharedResponseSchema, +} from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; +import { removeServerGeneratedProperties } from './remove_server_generated_properties'; -/** - * This is the typical output of a simple rule that Kibana will output with all the defaults - * except for the server generated properties. Useful for testing end to end tests. - */ -export const getSimpleRuleOutput = (ruleId = 'rule-1', enabled = false): Partial => ({ +export const getMockSharedResponseSchema = ( + ruleId = 'rule-1', + enabled = false +): SharedResponseSchema => ({ actions: [], author: [], created_by: 'elastic', @@ -20,10 +23,8 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1', enabled = false): Partial false_positives: [], from: 'now-6m', immutable: false, - index: ['auditbeat-*'], interval: '5m', rule_id: ruleId, - language: 'kuery', output_index: '', max_signals: 100, related_integrations: [], @@ -31,17 +32,50 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1', enabled = false): Partial risk_score: 1, risk_score_mapping: [], name: 'Simple Rule Query', - query: 'user.name: root or user.name: admin', references: [], setup: '', - severity: 'high', + severity: 'high' as const, severity_mapping: [], updated_by: 'elastic', tags: [], to: 'now', - type: 'query', threat: [], throttle: 'no_actions', exceptions_list: [], version: 1, + id: 'id', + updated_at: '2020-07-08T16:36:32.377Z', + created_at: '2020-07-08T16:36:32.377Z', + building_block_type: undefined, + note: undefined, + license: undefined, + outcome: undefined, + alias_target_id: undefined, + alias_purpose: undefined, + timeline_id: undefined, + timeline_title: undefined, + meta: undefined, + rule_name_override: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + namespace: undefined, }); + +const getQueryRuleOutput = (ruleId = 'rule-1', enabled = false): FullResponseSchema => ({ + ...getMockSharedResponseSchema(ruleId, enabled), + index: ['auditbeat-*'], + language: 'kuery', + query: 'user.name: root or user.name: admin', + type: 'query', + data_view_id: undefined, + filters: undefined, + saved_id: undefined, +}); + +/** + * This is the typical output of a simple rule that Kibana will output with all the defaults + * except for the server generated properties. Useful for testing end to end tests. + */ +export const getSimpleRuleOutput = (ruleId = 'rule-1', enabled = false) => { + return removeServerGeneratedProperties(getQueryRuleOutput(ruleId, enabled)); +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts index 45dd0bfd5d477..c96537bfd0813 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts @@ -5,10 +5,12 @@ * 2.0. */ -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; import { getSimpleRuleOutput } from './get_simple_rule_output'; +import { RuleWithoutServerGeneratedProperties } from './remove_server_generated_properties'; -export const getSimpleRuleOutputWithWebHookAction = (actionId: string): Partial => ({ +export const getSimpleRuleOutputWithWebHookAction = ( + actionId: string +): RuleWithoutServerGeneratedProperties => ({ ...getSimpleRuleOutput(), throttle: 'rule', actions: [ diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_without_rule_id.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_without_rule_id.ts index dbf94965278d6..56b5ab66773bb 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_without_rule_id.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_without_rule_id.ts @@ -5,14 +5,16 @@ * 2.0. */ -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; import { getSimpleRuleOutput } from './get_simple_rule_output'; +import { RuleWithoutServerGeneratedProperties } from './remove_server_generated_properties'; /** * This is the typical output of a simple rule that Kibana will output with all the defaults except * for all the server generated properties such as created_by. Useful for testing end to end tests. */ -export const getSimpleRuleOutputWithoutRuleId = (ruleId = 'rule-1'): Partial => { +export const getSimpleRuleOutputWithoutRuleId = ( + ruleId = 'rule-1' +): Omit => { const rule = getSimpleRuleOutput(ruleId); const { rule_id: rId, ...ruleWithoutRuleId } = rule; return ruleWithoutRuleId; diff --git a/x-pack/test/detection_engine_api_integration/utils/remove_server_generated_properties.ts b/x-pack/test/detection_engine_api_integration/utils/remove_server_generated_properties.ts index 5f863c0e62b9b..8d8a34bba8b79 100644 --- a/x-pack/test/detection_engine_api_integration/utils/remove_server_generated_properties.ts +++ b/x-pack/test/detection_engine_api_integration/utils/remove_server_generated_properties.ts @@ -6,6 +6,15 @@ */ import type { FullResponseSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/request'; +import { omit, pickBy } from 'lodash'; + +const serverGeneratedProperties = ['id', 'created_at', 'updated_at', 'execution_summary'] as const; + +type ServerGeneratedProperties = typeof serverGeneratedProperties[number]; +export type RuleWithoutServerGeneratedProperties = Omit< + FullResponseSchema, + ServerGeneratedProperties +>; /** * This will remove server generated properties such as date times, etc... @@ -13,14 +22,12 @@ import type { FullResponseSchema } from '@kbn/security-solution-plugin/common/de */ export const removeServerGeneratedProperties = ( rule: FullResponseSchema -): Partial => { - const { - /* eslint-disable @typescript-eslint/naming-convention */ - id, - created_at, - updated_at, - execution_summary, - ...removedProperties - } = rule; - return removedProperties; +): RuleWithoutServerGeneratedProperties => { + const removedProperties = omit(rule, serverGeneratedProperties); + + // We're only removing undefined values, so this cast correctly narrows the type + return pickBy( + removedProperties, + (value) => value !== undefined + ) as RuleWithoutServerGeneratedProperties; }; diff --git a/x-pack/test/detection_engine_api_integration/utils/resolve_simple_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/resolve_simple_rule_output.ts index 468cbdfa23aa5..4f8b24e623ac3 100644 --- a/x-pack/test/detection_engine_api_integration/utils/resolve_simple_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/resolve_simple_rule_output.ts @@ -5,11 +5,9 @@ * 2.0. */ -import type { RulesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response/rules_schema'; - import { getSimpleRuleOutput } from './get_simple_rule_output'; -export const resolveSimpleRuleOutput = ( - ruleId = 'rule-1', - enabled = false -): Partial => ({ outcome: 'exactMatch', ...getSimpleRuleOutput(ruleId, enabled) }); +export const resolveSimpleRuleOutput = (ruleId = 'rule-1', enabled = false) => ({ + ...getSimpleRuleOutput(ruleId, enabled), + outcome: 'exactMatch', +});