[Security Solution][Detections] Adds framework for replacing API schemas (#82462) (#83367)

* Adds framework for replacing API schemas

* Update integration tests with new schema

* Fix response type on createRule helper

* Add unit tests for new rule schema, add defaults for some array fields, clean up API schema definitions

* Naming updates and linting fixes

* Replace create_rules_bulk_schema and refactor route

* Convert update_rules_route to new schema

* Fix missing name error

* Fix more tests

* Fix import

* Update patch route with internal schema validation

* Reorganize new schema as drop-in replacement for create_rules_schema

* Replace updateRulesSchema with new version

* Cleanup - remove references to specific files within request folder

* Fix imports

* Fix tests

* Allow a few more fields to be undefined in internal schema

* Add static types back to test payloads, add more tests, add NonEmptyArray type builder

* Pull defaults into reusable function
This commit is contained in:
Marshall Main 2020-11-12 23:52:07 -05:00 committed by GitHub
parent c19d74c508
commit 79f9df06cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 1921 additions and 4261 deletions

View file

@ -9,6 +9,11 @@
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import {
SavedObjectAttributes,
SavedObjectAttribute,
SavedObjectAttributeSingle,
} from 'src/core/types';
import { RiskScore } from '../types/risk_score';
import { UUID } from '../types/uuid';
import { IsoDateString } from '../types/iso_date_string';
@ -66,6 +71,22 @@ export type ExcludeExportDetails = t.TypeOf<typeof exclude_export_details>;
export const filters = t.array(t.unknown); // Filters are not easily type-able yet
export type Filters = t.TypeOf<typeof filters>; // Filters are not easily type-able yet
export const filtersOrUndefined = t.union([filters, t.undefined]);
export type FiltersOrUndefined = t.TypeOf<typeof filtersOrUndefined>;
export const saved_object_attribute_single: t.Type<SavedObjectAttributeSingle> = t.recursion(
'saved_object_attribute_single',
() => t.union([t.string, t.number, t.boolean, t.null, t.undefined, saved_object_attributes])
);
export const saved_object_attribute: t.Type<SavedObjectAttribute> = t.recursion(
'saved_object_attribute',
() => t.union([saved_object_attribute_single, t.array(saved_object_attribute_single)])
);
export const saved_object_attributes: t.Type<SavedObjectAttributes> = t.recursion(
'saved_object_attributes',
() => t.record(t.string, saved_object_attribute)
);
/**
* Params is an "object", since it is a type of AlertActionParams which is action templates.
* @see x-pack/plugins/alerts/common/alert.ts
@ -73,7 +94,7 @@ export type Filters = t.TypeOf<typeof filters>; // Filters are not easily type-a
export const action_group = t.string;
export const action_id = t.string;
export const action_action_type_id = t.string;
export const action_params = t.object;
export const action_params = saved_object_attributes;
export const action = t.exact(
t.type({
group: action_group,
@ -86,6 +107,18 @@ export const action = t.exact(
export const actions = t.array(action);
export type Actions = t.TypeOf<typeof actions>;
export const actionsCamel = t.array(
t.exact(
t.type({
group: action_group,
id: action_id,
actionTypeId: action_action_type_id,
params: action_params,
})
)
);
export type ActionsCamel = t.TypeOf<typeof actions>;
const stringValidator = (input: unknown): input is string => typeof input === 'string';
export const from = new t.Type<string, string, unknown>(
'From',
@ -416,6 +449,10 @@ export const created_at = IsoDateString;
export const updated_at = IsoDateString;
export const updated_by = t.string;
export const created_by = t.string;
export const updatedByOrNull = t.union([updated_by, t.null]);
export type UpdatedByOrNull = t.TypeOf<typeof updatedByOrNull>;
export const createdByOrNull = t.union([created_by, t.null]);
export type CreatedByOrNull = t.TypeOf<typeof createdByOrNull>;
export const version = PositiveIntegerGreaterThanZero;
export type Version = t.TypeOf<typeof version>;

View file

@ -4,22 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
createRulesBulkSchema,
CreateRulesBulkSchema,
CreateRulesBulkSchemaDecoded,
} from './create_rules_bulk_schema';
import { createRulesBulkSchema, CreateRulesBulkSchema } from './create_rules_bulk_schema';
import { exactCheck } from '../../../exact_check';
import { foldLeftRight } from '../../../test_utils';
import {
getCreateRulesSchemaMock,
getCreateRulesSchemaDecodedMock,
} from './create_rules_schema.mock';
import { formatErrors } from '../../../format_errors';
import { CreateRulesSchema } from './create_rules_schema';
import { getCreateRulesSchemaMock } from './rule_schemas.mock';
// only the basics of testing are here.
// see: create_rules_schema.test.ts for the bulk of the validation tests
// see: rule_schemas.test.ts for the bulk of the validation tests
// this just wraps createRulesSchema in an array
describe('create_rules_bulk_schema', () => {
test('can take an empty array and validate it', () => {
@ -38,13 +30,16 @@ describe('create_rules_bulk_schema', () => {
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "description"',
'Invalid value "undefined" supplied to "risk_score"',
'Invalid value "undefined" supplied to "name"',
'Invalid value "undefined" supplied to "severity"',
'Invalid value "undefined" supplied to "type"',
]);
expect(formatErrors(output.errors)).toContain(
'Invalid value "undefined" supplied to "description"'
);
expect(formatErrors(output.errors)).toContain(
'Invalid value "undefined" supplied to "risk_score"'
);
expect(formatErrors(output.errors)).toContain('Invalid value "undefined" supplied to "name"');
expect(formatErrors(output.errors)).toContain(
'Invalid value "undefined" supplied to "severity"'
);
expect(output.schema).toEqual({});
});
@ -55,7 +50,7 @@ describe('create_rules_bulk_schema', () => {
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([getCreateRulesSchemaDecodedMock()]);
expect(output.schema).toEqual(payload);
});
test('two array elements do validate', () => {
@ -65,10 +60,7 @@ describe('create_rules_bulk_schema', () => {
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([
getCreateRulesSchemaDecodedMock(),
getCreateRulesSchemaDecodedMock(),
]);
expect(output.schema).toEqual(payload);
});
test('single array element with a missing value (risk_score) will not validate', () => {
@ -137,7 +129,7 @@ describe('create_rules_bulk_schema', () => {
});
test('two array elements where the first is invalid (extra key and value) but the second is valid will not validate', () => {
const singleItem: CreateRulesSchema & { madeUpValue: string } = {
const singleItem = {
...getCreateRulesSchemaMock(),
madeUpValue: 'something',
};
@ -152,8 +144,8 @@ describe('create_rules_bulk_schema', () => {
});
test('two array elements where the second is invalid (extra key and value) but the first is valid will not validate', () => {
const singleItem: CreateRulesSchema = getCreateRulesSchemaMock();
const secondItem: CreateRulesSchema & { madeUpValue: string } = {
const singleItem = getCreateRulesSchemaMock();
const secondItem = {
...getCreateRulesSchemaMock(),
madeUpValue: 'something',
};
@ -167,11 +159,11 @@ describe('create_rules_bulk_schema', () => {
});
test('two array elements where both are invalid (extra key and value) will not validate', () => {
const singleItem: CreateRulesSchema & { madeUpValue: string } = {
const singleItem = {
...getCreateRulesSchemaMock(),
madeUpValue: 'something',
};
const secondItem: CreateRulesSchema & { madeUpValue: string } = {
const secondItem = {
...getCreateRulesSchemaMock(),
madeUpValue: 'something',
};
@ -184,28 +176,6 @@ describe('create_rules_bulk_schema', () => {
expect(output.schema).toEqual({});
});
test('The default for "from" will be "now-6m"', () => {
const { from, ...withoutFrom } = getCreateRulesSchemaMock();
const payload: CreateRulesBulkSchema = [withoutFrom];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect((output.schema as CreateRulesBulkSchemaDecoded)[0].from).toEqual('now-6m');
});
test('The default for "to" will be "now"', () => {
const { to, ...withoutTo } = getCreateRulesSchemaMock();
const payload: CreateRulesBulkSchema = [withoutTo];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect((output.schema as CreateRulesBulkSchemaDecoded)[0].to).toEqual('now');
});
test('You cannot set the severity to a value other than low, medium, high, or critical', () => {
const badSeverity = { ...getCreateRulesSchemaMock(), severity: 'madeup' };
const payload = [badSeverity];
@ -226,9 +196,7 @@ describe('create_rules_bulk_schema', () => {
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([
{ ...getCreateRulesSchemaDecodedMock(), note: '# test markdown' },
]);
expect(output.schema).toEqual(payload);
});
test('You can set "note" to an empty string', () => {
@ -238,10 +206,10 @@ describe('create_rules_bulk_schema', () => {
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([{ ...getCreateRulesSchemaDecodedMock(), note: '' }]);
expect(output.schema).toEqual(payload);
});
test('You can set "note" to anything other than string', () => {
test('You cant set "note" to anything other than string', () => {
const payload = [
{
...getCreateRulesSchemaMock(),
@ -259,26 +227,4 @@ describe('create_rules_bulk_schema', () => {
]);
expect(output.schema).toEqual({});
});
test('The default for "actions" will be an empty array', () => {
const { actions, ...withoutActions } = getCreateRulesSchemaMock();
const payload: CreateRulesBulkSchema = [withoutActions];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect((output.schema as CreateRulesBulkSchemaDecoded)[0].actions).toEqual([]);
});
test('The default for "throttle" will be null', () => {
const { throttle, ...withoutThrottle } = getCreateRulesSchemaMock();
const payload: CreateRulesBulkSchema = [withoutThrottle];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect((output.schema as CreateRulesBulkSchemaDecoded)[0].throttle).toEqual(null);
});
});

View file

@ -6,9 +6,7 @@
import * as t from 'io-ts';
import { createRulesSchema, CreateRulesSchemaDecoded } from './create_rules_schema';
import { createRulesSchema } from './rule_schemas';
export const createRulesBulkSchema = t.array(createRulesSchema);
export type CreateRulesBulkSchema = t.TypeOf<typeof createRulesBulkSchema>;
export type CreateRulesBulkSchemaDecoded = CreateRulesSchemaDecoded[];

View file

@ -1,157 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CreateRulesSchema, CreateRulesSchemaDecoded } from './create_rules_schema';
import { DEFAULT_MAX_SIGNALS } from '../../../constants';
export const getCreateRulesSchemaMock = (ruleId = 'rule-1'): CreateRulesSchema => ({
description: 'Detecting root and admin users',
name: 'Query with a rule id',
query: 'user.name: root or user.name: admin',
severity: 'high',
type: 'query',
risk_score: 55,
language: 'kuery',
rule_id: ruleId,
});
export const getCreateMlRulesSchemaMock = (ruleId = 'rule-1') => {
const { query, language, index, ...mlParams } = getCreateRulesSchemaMock(ruleId);
return {
...mlParams,
type: 'machine_learning',
anomaly_threshold: 58,
machine_learning_job_id: 'typical-ml-job-id',
};
};
export const getCreateRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => ({
author: [],
severity_mapping: [],
risk_score_mapping: [],
description: 'Detecting root and admin users',
name: 'Query with a rule id',
query: 'user.name: root or user.name: admin',
severity: 'high',
type: 'query',
risk_score: 55,
language: 'kuery',
references: [],
actions: [],
enabled: true,
false_positives: [],
from: 'now-6m',
interval: '5m',
max_signals: DEFAULT_MAX_SIGNALS,
tags: [],
to: 'now',
threat: [],
throttle: null,
version: 1,
exceptions_list: [],
rule_id: 'rule-1',
});
export const getCreateThreatMatchRulesSchemaMock = (ruleId = 'rule-1'): CreateRulesSchema => ({
description: 'Detecting root and admin users',
name: 'Query with a rule id',
query: 'user.name: root or user.name: admin',
severity: 'high',
type: 'threat_match',
risk_score: 55,
language: 'kuery',
rule_id: ruleId,
threat_query: '*:*',
threat_index: ['list-index'],
threat_mapping: [
{
entries: [
{
field: 'host.name',
value: 'host.name',
type: 'mapping',
},
],
},
],
threat_filters: [
{
bool: {
must: [
{
query_string: {
query: 'host.name: linux',
analyze_wildcard: true,
time_zone: 'Zulu',
},
},
],
filter: [],
should: [],
must_not: [],
},
},
],
});
export const getCreateThreatMatchRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => ({
author: [],
severity_mapping: [],
risk_score_mapping: [],
description: 'Detecting root and admin users',
name: 'Query with a rule id',
query: 'user.name: root or user.name: admin',
severity: 'high',
type: 'threat_match',
risk_score: 55,
language: 'kuery',
references: [],
actions: [],
enabled: true,
false_positives: [],
from: 'now-6m',
interval: '5m',
max_signals: DEFAULT_MAX_SIGNALS,
tags: [],
to: 'now',
threat: [],
throttle: null,
version: 1,
exceptions_list: [],
rule_id: 'rule-1',
threat_query: '*:*',
threat_index: ['list-index'],
threat_mapping: [
{
entries: [
{
field: 'host.name',
value: 'host.name',
type: 'mapping',
},
],
},
],
threat_filters: [
{
bool: {
must: [
{
query_string: {
query: 'host.name: linux',
analyze_wildcard: true,
time_zone: 'Zulu',
},
},
],
filter: [],
should: [],
must_not: [],
},
},
],
});

View file

@ -1,177 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import {
description,
anomaly_threshold,
building_block_type,
filters,
RuleId,
index,
output_index,
saved_id,
timeline_id,
timeline_title,
meta,
machine_learning_job_id,
risk_score,
MaxSignals,
name,
severity,
Tags,
To,
type,
Threat,
threshold,
ThrottleOrNull,
note,
Version,
References,
Actions,
Enabled,
FalsePositives,
From,
Interval,
language,
query,
license,
rule_name_override,
timestamp_override,
Author,
RiskScoreMapping,
SeverityMapping,
event_category_override,
} from '../common/schemas';
import {
threat_index,
concurrent_searches,
items_per_search,
threat_query,
threat_filters,
threat_mapping,
threat_language,
} from '../types/threat_mapping';
import {
DefaultStringArray,
DefaultActionsArray,
DefaultBooleanTrue,
DefaultFromString,
DefaultIntervalString,
DefaultMaxSignalsNumber,
DefaultToString,
DefaultThreatArray,
DefaultThrottleNull,
DefaultVersionNumber,
DefaultListArray,
ListArray,
DefaultUuid,
DefaultRiskScoreMappingArray,
DefaultSeverityMappingArray,
} from '../types';
export const createRulesSchema = t.intersection([
t.exact(
t.type({
description,
risk_score,
name,
severity,
type,
})
),
t.exact(
t.partial({
actions: DefaultActionsArray, // defaults to empty actions array if not set during decode
anomaly_threshold, // defaults to undefined if not set during decode
author: DefaultStringArray, // defaults to empty array of strings if not set during decode
building_block_type, // defaults to undefined if not set during decode
enabled: DefaultBooleanTrue, // defaults to true if not set during decode
event_category_override, // defaults to "undefined" if not set during decode
false_positives: DefaultStringArray, // defaults to empty string array if not set during decode
filters, // defaults to undefined if not set during decode
from: DefaultFromString, // defaults to "now-6m" if not set during decode
rule_id: DefaultUuid,
index, // defaults to undefined if not set during decode
interval: DefaultIntervalString, // defaults to "5m" if not set during decode
query, // defaults to undefined if not set during decode
language, // defaults to undefined if not set during decode
license, // defaults to "undefined" if not set during decode
// TODO: output_index: This should be removed eventually
output_index, // defaults to "undefined" if not set during decode
saved_id, // defaults to "undefined" if not set during decode
timeline_id, // defaults to "undefined" if not set during decode
timeline_title, // defaults to "undefined" if not set during decode
meta, // defaults to "undefined" if not set during decode
machine_learning_job_id, // defaults to "undefined" if not set during decode
max_signals: DefaultMaxSignalsNumber, // defaults to DEFAULT_MAX_SIGNALS (100) if not set during decode
risk_score_mapping: DefaultRiskScoreMappingArray, // defaults to empty risk score mapping array if not set during decode
rule_name_override, // defaults to "undefined" if not set during decode
severity_mapping: DefaultSeverityMappingArray, // defaults to empty actions array if not set during decode
tags: DefaultStringArray, // defaults to empty string array if not set during decode
to: DefaultToString, // defaults to "now" if not set during decode
threat: DefaultThreatArray, // defaults to empty array if not set during decode
threshold, // defaults to "undefined" if not set during decode
throttle: DefaultThrottleNull, // defaults to "null" if not set during decode
timestamp_override, // defaults to "undefined" if not set during decode
references: DefaultStringArray, // defaults to empty array of strings if not set during decode
note, // defaults to "undefined" if not set during decode
version: DefaultVersionNumber, // defaults to 1 if not set during decode
exceptions_list: DefaultListArray, // defaults to empty array if not set during decode
threat_mapping, // defaults to "undefined" if not set during decode
threat_query, // defaults to "undefined" if not set during decode
threat_filters, // defaults to "undefined" if not set during decode
threat_index, // defaults to "undefined" if not set during decode
threat_language, // defaults "undefined" if not set during decode
concurrent_searches, // defaults "undefined" if not set during decode
items_per_search, // defaults "undefined" if not set during decode
})
),
]);
export type CreateRulesSchema = t.TypeOf<typeof createRulesSchema>;
// This type is used after a decode since some things are defaults after a decode.
export type CreateRulesSchemaDecoded = Omit<
CreateRulesSchema,
| 'author'
| 'references'
| 'actions'
| 'enabled'
| 'false_positives'
| 'from'
| 'interval'
| 'max_signals'
| 'risk_score_mapping'
| 'severity_mapping'
| 'tags'
| 'to'
| 'threat'
| 'throttle'
| 'version'
| 'exceptions_list'
| 'rule_id'
> & {
author: Author;
references: References;
actions: Actions;
enabled: Enabled;
false_positives: FalsePositives;
from: From;
interval: Interval;
max_signals: MaxSignals;
risk_score_mapping: RiskScoreMapping;
severity_mapping: SeverityMapping;
tags: Tags;
to: To;
threat: Threat;
throttle: ThrottleOrNull;
version: Version;
exceptions_list: ListArray;
rule_id: RuleId;
};

View file

@ -4,31 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
getCreateRulesSchemaMock,
getCreateThreatMatchRulesSchemaMock,
} from './create_rules_schema.mock';
import { CreateRulesSchema } from './create_rules_schema';
import { getCreateRulesSchemaMock, getCreateThreatMatchRulesSchemaMock } from './rule_schemas.mock';
import { CreateRulesSchema } from './rule_schemas';
import { createRuleValidateTypeDependents } from './create_rules_type_dependents';
describe('create_rules_type_dependents', () => {
test('saved_id is required when type is saved_query and will not validate without out', () => {
const schema: CreateRulesSchema = { ...getCreateRulesSchemaMock(), type: 'saved_query' };
delete schema.saved_id;
const errors = createRuleValidateTypeDependents(schema);
expect(errors).toEqual(['when "type" is "saved_query", "saved_id" is required']);
});
test('saved_id is required when type is saved_query and validates with it', () => {
const schema: CreateRulesSchema = {
...getCreateRulesSchemaMock(),
type: 'saved_query',
saved_id: '123',
};
const errors = createRuleValidateTypeDependents(schema);
expect(errors).toEqual([]);
});
test('You cannot omit timeline_title when timeline_id is present', () => {
const schema: CreateRulesSchema = {
...getCreateRulesSchemaMock(),
@ -69,63 +49,6 @@ describe('create_rules_type_dependents', () => {
expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']);
});
test('threshold is required when type is threshold and validates with it', () => {
const schema: CreateRulesSchema = {
...getCreateRulesSchemaMock(),
type: 'threshold',
};
const errors = createRuleValidateTypeDependents(schema);
expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']);
});
test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => {
const schema: CreateRulesSchema = {
...getCreateRulesSchemaMock(),
type: 'threshold',
threshold: {
field: '',
value: -1,
},
};
const errors = createRuleValidateTypeDependents(schema);
expect(errors).toEqual(['"threshold.value" has to be bigger than 0']);
});
test('threat_index, threat_query, and threat_mapping are required when type is "threat_match" and validates with it', () => {
const schema: CreateRulesSchema = {
...getCreateRulesSchemaMock(),
type: 'threat_match',
};
const errors = createRuleValidateTypeDependents(schema);
expect(errors).toEqual([
'when "type" is "threat_match", "threat_index" is required',
'when "type" is "threat_match", "threat_query" is required',
'when "type" is "threat_match", "threat_mapping" is required',
]);
});
test('validates with threat_index, threat_query, and threat_mapping when type is "threat_match"', () => {
const schema = getCreateThreatMatchRulesSchemaMock();
const { threat_filters: threatFilters, ...noThreatFilters } = schema;
const errors = createRuleValidateTypeDependents(noThreatFilters);
expect(errors).toEqual([]);
});
test('does NOT validate when threat_mapping is an empty array', () => {
const schema: CreateRulesSchema = {
...getCreateThreatMatchRulesSchemaMock(),
threat_mapping: [],
};
const errors = createRuleValidateTypeDependents(schema);
expect(errors).toEqual(['threat_mapping" must have at least one element']);
});
test('validates with threat_index, threat_query, threat_mapping, and an optional threat_filters, when type is "threat_match"', () => {
const schema = getCreateThreatMatchRulesSchemaMock();
const errors = createRuleValidateTypeDependents(schema);
expect(errors).toEqual([]);
});
test('validates that both "items_per_search" and "concurrent_searches" works when together', () => {
const schema: CreateRulesSchema = {
...getCreateThreatMatchRulesSchemaMock(),

View file

@ -4,69 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { isMlRule } from '../../../machine_learning/helpers';
import { isThreatMatchRule, isThresholdRule } from '../../utils';
import { CreateRulesSchema } from './create_rules_schema';
export const validateAnomalyThreshold = (rule: CreateRulesSchema): string[] => {
if (isMlRule(rule.type)) {
if (rule.anomaly_threshold == null) {
return ['when "type" is "machine_learning" anomaly_threshold is required'];
} else {
return [];
}
} else {
return [];
}
};
export const validateQuery = (rule: CreateRulesSchema): string[] => {
if (isMlRule(rule.type)) {
if (rule.query != null) {
return ['when "type" is "machine_learning", "query" cannot be set'];
} else {
return [];
}
} else {
return [];
}
};
export const validateLanguage = (rule: CreateRulesSchema): string[] => {
if (isMlRule(rule.type)) {
if (rule.language != null) {
return ['when "type" is "machine_learning", "language" cannot be set'];
} else {
return [];
}
} else {
return [];
}
};
export const validateSavedId = (rule: CreateRulesSchema): string[] => {
if (rule.type === 'saved_query') {
if (rule.saved_id == null) {
return ['when "type" is "saved_query", "saved_id" is required'];
} else {
return [];
}
} else {
return [];
}
};
export const validateMachineLearningJobId = (rule: CreateRulesSchema): string[] => {
if (isMlRule(rule.type)) {
if (rule.machine_learning_job_id == null) {
return ['when "type" is "machine_learning", "machine_learning_job_id" is required'];
} else {
return [];
}
} else {
return [];
}
};
import { CreateRulesSchema } from './rule_schemas';
export const validateTimelineId = (rule: CreateRulesSchema): string[] => {
if (rule.timeline_id != null) {
@ -94,33 +32,9 @@ export const validateTimelineTitle = (rule: CreateRulesSchema): string[] => {
return [];
};
export const validateThreshold = (rule: CreateRulesSchema): string[] => {
if (isThresholdRule(rule.type)) {
if (!rule.threshold) {
return ['when "type" is "threshold", "threshold" is required'];
} else if (rule.threshold.value <= 0) {
return ['"threshold.value" has to be bigger than 0'];
} else {
return [];
}
}
return [];
};
export const validateThreatMapping = (rule: CreateRulesSchema): string[] => {
let errors: string[] = [];
if (isThreatMatchRule(rule.type)) {
if (rule.threat_mapping == null) {
errors = ['when "type" is "threat_match", "threat_mapping" is required', ...errors];
} else if (rule.threat_mapping.length === 0) {
errors = ['threat_mapping" must have at least one element', ...errors];
}
if (rule.threat_query == null) {
errors = ['when "type" is "threat_match", "threat_query" is required', ...errors];
}
if (rule.threat_index == null) {
errors = ['when "type" is "threat_match", "threat_index" is required', ...errors];
}
if (rule.type === 'threat_match') {
if (rule.concurrent_searches == null && rule.items_per_search != null) {
errors = ['when "items_per_search" exists, "concurrent_searches" must also exist', ...errors];
}
@ -133,14 +47,8 @@ export const validateThreatMapping = (rule: CreateRulesSchema): string[] => {
export const createRuleValidateTypeDependents = (schema: CreateRulesSchema): string[] => {
return [
...validateAnomalyThreshold(schema),
...validateQuery(schema),
...validateLanguage(schema),
...validateSavedId(schema),
...validateMachineLearningJobId(schema),
...validateTimelineId(schema),
...validateTimelineTitle(schema),
...validateThreshold(schema),
...validateThreatMapping(schema),
];
};

View file

@ -5,7 +5,6 @@
*/
export * from './add_prepackaged_rules_schema';
export * from './create_rules_bulk_schema';
export * from './create_rules_schema';
export * from './export_rules_schema';
export * from './find_rules_schema';
export * from './import_rules_schema';
@ -15,4 +14,4 @@ export * from './query_rules_schema';
export * from './query_signals_index_schema';
export * from './set_signal_status_schema';
export * from './update_rules_bulk_schema';
export * from './update_rules_schema';
export * from './rule_schemas';

View file

@ -0,0 +1,195 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
MachineLearningCreateSchema,
MachineLearningUpdateSchema,
QueryCreateSchema,
QueryUpdateSchema,
SavedQueryCreateSchema,
SavedQueryUpdateSchema,
ThreatMatchCreateSchema,
ThreatMatchUpdateSchema,
ThresholdCreateSchema,
} from './rule_schemas';
export const getCreateRulesSchemaMock = (ruleId = 'rule-1'): QueryCreateSchema => ({
description: 'Detecting root and admin users',
name: 'Query with a rule id',
query: 'user.name: root or user.name: admin',
severity: 'high',
type: 'query',
risk_score: 55,
language: 'kuery',
rule_id: ruleId,
});
export const getCreateSavedQueryRulesSchemaMock = (ruleId = 'rule-1'): SavedQueryCreateSchema => ({
description: 'Detecting root and admin users',
name: 'Query with a rule id',
query: 'user.name: root or user.name: admin',
severity: 'high',
type: 'saved_query',
saved_id: 'some id',
risk_score: 55,
language: 'kuery',
rule_id: ruleId,
});
export const getCreateThreatMatchRulesSchemaMock = (
ruleId = 'rule-1'
): ThreatMatchCreateSchema => ({
description: 'Detecting root and admin users',
name: 'Query with a rule id',
query: 'user.name: root or user.name: admin',
severity: 'high',
type: 'threat_match',
risk_score: 55,
language: 'kuery',
rule_id: ruleId,
threat_query: '*:*',
threat_index: ['list-index'],
threat_mapping: [
{
entries: [
{
field: 'host.name',
value: 'host.name',
type: 'mapping',
},
],
},
],
threat_filters: [
{
bool: {
must: [
{
query_string: {
query: 'host.name: linux',
analyze_wildcard: true,
time_zone: 'Zulu',
},
},
],
filter: [],
should: [],
must_not: [],
},
},
],
});
export const getCreateMachineLearningRulesSchemaMock = (
ruleId = 'rule-1'
): MachineLearningCreateSchema => ({
description: 'Detecting root and admin users',
name: 'Query with a rule id',
severity: 'high',
risk_score: 55,
rule_id: ruleId,
type: 'machine_learning',
anomaly_threshold: 58,
machine_learning_job_id: 'typical-ml-job-id',
});
export const getCreateThresholdRulesSchemaMock = (ruleId = 'rule-1'): ThresholdCreateSchema => ({
description: 'Detecting root and admin users',
name: 'Query with a rule id',
severity: 'high',
risk_score: 55,
rule_id: ruleId,
type: 'threshold',
query: 'user.name: root or user.name: admin',
threshold: {
field: 'some.field',
value: 4,
},
});
export const getUpdateRulesSchemaMock = (
id = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'
): QueryUpdateSchema => ({
description: 'Detecting root and admin users',
name: 'Query with a rule id',
query: 'user.name: root or user.name: admin',
severity: 'high',
type: 'query',
risk_score: 55,
language: 'kuery',
id,
});
export const getUpdateSavedQuerySchemaMock = (
id = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'
): SavedQueryUpdateSchema => ({
description: 'Detecting root and admin users',
name: 'Query with a rule id',
query: 'user.name: root or user.name: admin',
severity: 'high',
type: 'saved_query',
saved_id: 'some id',
risk_score: 55,
language: 'kuery',
id,
});
export const getUpdateThreatMatchSchemaMock = (
id = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'
): ThreatMatchUpdateSchema => ({
description: 'Detecting root and admin users',
name: 'Query with a rule id',
query: 'user.name: root or user.name: admin',
severity: 'high',
type: 'threat_match',
risk_score: 55,
language: 'kuery',
id,
threat_query: '*:*',
threat_index: ['list-index'],
threat_mapping: [
{
entries: [
{
field: 'host.name',
value: 'host.name',
type: 'mapping',
},
],
},
],
threat_filters: [
{
bool: {
must: [
{
query_string: {
query: 'host.name: linux',
analyze_wildcard: true,
time_zone: 'Zulu',
},
},
],
filter: [],
should: [],
must_not: [],
},
},
],
});
export const getUpdateMachineLearningSchemaMock = (
id = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'
): MachineLearningUpdateSchema => ({
description: 'Detecting root and admin users',
name: 'Query with a rule id',
severity: 'high',
risk_score: 55,
id,
type: 'machine_learning',
anomaly_threshold: 58,
machine_learning_job_id: 'typical-ml-job-id',
});

View file

@ -0,0 +1,442 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { listArray } from '../types/lists';
import {
threat_filters,
threat_query,
threat_mapping,
threat_index,
concurrent_searches,
items_per_search,
} from '../types/threat_mapping';
import {
id,
index,
filters,
event_category_override,
risk_score_mapping,
severity_mapping,
building_block_type,
note,
license,
timeline_id,
timeline_title,
meta,
rule_name_override,
timestamp_override,
author,
description,
false_positives,
from,
rule_id,
immutable,
output_index,
query,
machine_learning_job_id,
max_signals,
risk_score,
severity,
threat,
to,
references,
version,
saved_id,
threshold,
anomaly_threshold,
name,
tags,
actions,
interval,
enabled,
updated_at,
created_at,
job_status,
status_date,
last_success_at,
last_success_message,
last_failure_at,
last_failure_message,
throttleOrNull,
createdByOrNull,
updatedByOrNull,
} from '../common/schemas';
const createSchema = <
Required extends t.Props,
Optional extends t.Props,
Defaultable extends t.Props
>(
requiredFields: Required,
optionalFields: Optional,
defaultableFields: Defaultable
) => {
return t.intersection([
t.exact(t.type(requiredFields)),
t.exact(t.partial(optionalFields)),
t.exact(t.partial(defaultableFields)),
]);
};
const patchSchema = <
Required extends t.Props,
Optional extends t.Props,
Defaultable extends t.Props
>(
requiredFields: Required,
optionalFields: Optional,
defaultableFields: Defaultable
) => {
return t.intersection([
t.exact(t.partial(requiredFields)),
t.exact(t.partial(optionalFields)),
t.exact(t.partial(defaultableFields)),
]);
};
const responseSchema = <
Required extends t.Props,
Optional extends t.Props,
Defaultable extends t.Props
>(
requiredFields: Required,
optionalFields: Optional,
defaultableFields: Defaultable
) => {
return t.intersection([
t.exact(t.type(requiredFields)),
t.exact(t.partial(optionalFields)),
t.exact(t.type(defaultableFields)),
]);
};
const buildAPISchemas = <R extends t.Props, O extends t.Props, D extends t.Props>(
params: APIParams<R, O, D>
) => {
return {
create: createSchema(params.required, params.optional, params.defaultable),
patch: patchSchema(params.required, params.optional, params.defaultable),
response: responseSchema(params.required, params.optional, params.defaultable),
};
};
interface APIParams<
Required extends t.Props,
Optional extends t.Props,
Defaultable extends t.Props
> {
required: Required;
optional: Optional;
defaultable: Defaultable;
}
const commonParams = {
required: {
name,
description,
risk_score,
severity,
},
optional: {
building_block_type,
note,
license,
output_index,
timeline_id,
timeline_title,
meta,
rule_name_override,
timestamp_override,
},
defaultable: {
tags,
interval,
enabled,
throttle: throttleOrNull,
actions,
author,
false_positives,
from,
rule_id,
// maxSignals not used in ML rules but probably should be used
max_signals,
risk_score_mapping,
severity_mapping,
threat,
to,
references,
version,
exceptions_list: listArray,
},
};
const {
create: commonCreateParams,
patch: commonPatchParams,
response: commonResponseParams,
} = buildAPISchemas(commonParams);
const eqlRuleParams = {
required: {
type: t.literal('eql'),
language: t.literal('eql'),
query,
},
optional: {
index,
filters,
event_category_override,
},
defaultable: {},
};
const {
create: eqlCreateParams,
patch: eqlPatchParams,
response: eqlResponseParams,
} = buildAPISchemas(eqlRuleParams);
const threatMatchRuleParams = {
required: {
type: t.literal('threat_match'),
query,
threat_query,
threat_mapping,
threat_index,
},
optional: {
index,
filters,
saved_id,
threat_filters,
threat_language: t.keyof({ kuery: null, lucene: null }),
concurrent_searches,
items_per_search,
},
defaultable: {
language: t.keyof({ kuery: null, lucene: null }),
},
};
const {
create: threatMatchCreateParams,
patch: threatMatchPatchParams,
response: threatMatchResponseParams,
} = buildAPISchemas(threatMatchRuleParams);
const queryRuleParams = {
required: {
type: t.literal('query'),
},
optional: {
index,
filters,
saved_id,
},
defaultable: {
query,
language: t.keyof({ kuery: null, lucene: null }),
},
};
const {
create: queryCreateParams,
patch: queryPatchParams,
response: queryResponseParams,
} = buildAPISchemas(queryRuleParams);
const savedQueryRuleParams = {
required: {
type: t.literal('saved_query'),
saved_id,
},
optional: {
// Having language, query, and filters possibly defined adds more code confusion and probably user confusion
// if the saved object gets deleted for some reason
index,
query,
filters,
},
defaultable: {
language: t.keyof({ kuery: null, lucene: null }),
},
};
const {
create: savedQueryCreateParams,
patch: savedQueryPatchParams,
response: savedQueryResponseParams,
} = buildAPISchemas(savedQueryRuleParams);
const thresholdRuleParams = {
required: {
type: t.literal('threshold'),
query,
threshold,
},
optional: {
index,
filters,
saved_id,
},
defaultable: {
language: t.keyof({ kuery: null, lucene: null }),
},
};
const {
create: thresholdCreateParams,
patch: thresholdPatchParams,
response: thresholdResponseParams,
} = buildAPISchemas(thresholdRuleParams);
const machineLearningRuleParams = {
required: {
type: t.literal('machine_learning'),
anomaly_threshold,
machine_learning_job_id,
},
optional: {},
defaultable: {},
};
const {
create: machineLearningCreateParams,
patch: machineLearningPatchParams,
response: machineLearningResponseParams,
} = buildAPISchemas(machineLearningRuleParams);
const createTypeSpecific = t.union([
eqlCreateParams,
threatMatchCreateParams,
queryCreateParams,
savedQueryCreateParams,
thresholdCreateParams,
machineLearningCreateParams,
]);
export type CreateTypeSpecific = t.TypeOf<typeof createTypeSpecific>;
// Convenience types for building specific types of rules
export const eqlCreateSchema = t.intersection([eqlCreateParams, commonCreateParams]);
export type EqlCreateSchema = t.TypeOf<typeof eqlCreateSchema>;
export const threatMatchCreateSchema = t.intersection([
threatMatchCreateParams,
commonCreateParams,
]);
export type ThreatMatchCreateSchema = t.TypeOf<typeof threatMatchCreateSchema>;
export const queryCreateSchema = t.intersection([queryCreateParams, commonCreateParams]);
export type QueryCreateSchema = t.TypeOf<typeof queryCreateSchema>;
export const savedQueryCreateSchema = t.intersection([savedQueryCreateParams, commonCreateParams]);
export type SavedQueryCreateSchema = t.TypeOf<typeof savedQueryCreateSchema>;
export const thresholdCreateSchema = t.intersection([thresholdCreateParams, commonCreateParams]);
export type ThresholdCreateSchema = t.TypeOf<typeof thresholdCreateSchema>;
export const machineLearningCreateSchema = t.intersection([
machineLearningCreateParams,
commonCreateParams,
]);
export type MachineLearningCreateSchema = t.TypeOf<typeof machineLearningCreateSchema>;
export const createRulesSchema = t.intersection([commonCreateParams, createTypeSpecific]);
export type CreateRulesSchema = t.TypeOf<typeof createRulesSchema>;
export const eqlUpdateSchema = t.intersection([
eqlCreateParams,
commonCreateParams,
t.exact(t.partial({ id })),
]);
export type EqlUpdateSchema = t.TypeOf<typeof eqlUpdateSchema>;
export const threatMatchUpdateSchema = t.intersection([
threatMatchCreateParams,
commonCreateParams,
t.exact(t.partial({ id })),
]);
export type ThreatMatchUpdateSchema = t.TypeOf<typeof threatMatchUpdateSchema>;
export const queryUpdateSchema = t.intersection([
queryCreateParams,
commonCreateParams,
t.exact(t.partial({ id })),
]);
export type QueryUpdateSchema = t.TypeOf<typeof queryUpdateSchema>;
export const savedQueryUpdateSchema = t.intersection([
savedQueryCreateParams,
commonCreateParams,
t.exact(t.partial({ id })),
]);
export type SavedQueryUpdateSchema = t.TypeOf<typeof savedQueryUpdateSchema>;
export const thresholdUpdateSchema = t.intersection([
thresholdCreateParams,
commonCreateParams,
t.exact(t.partial({ id })),
]);
export type ThresholdUpdateSchema = t.TypeOf<typeof thresholdUpdateSchema>;
export const machineLearningUpdateSchema = t.intersection([
machineLearningCreateParams,
commonCreateParams,
t.exact(t.partial({ id })),
]);
export type MachineLearningUpdateSchema = t.TypeOf<typeof machineLearningUpdateSchema>;
const patchTypeSpecific = t.union([
eqlPatchParams,
threatMatchPatchParams,
queryPatchParams,
savedQueryPatchParams,
thresholdPatchParams,
machineLearningPatchParams,
]);
const responseTypeSpecific = t.union([
eqlResponseParams,
threatMatchResponseParams,
queryResponseParams,
savedQueryResponseParams,
thresholdResponseParams,
machineLearningResponseParams,
]);
export type ResponseTypeSpecific = t.TypeOf<typeof responseTypeSpecific>;
export const updateRulesSchema = t.intersection([
commonCreateParams,
createTypeSpecific,
t.exact(t.partial({ id })),
]);
export type UpdateRulesSchema = t.TypeOf<typeof updateRulesSchema>;
export const fullPatchSchema = t.intersection([
commonPatchParams,
patchTypeSpecific,
t.exact(t.partial({ id })),
]);
const responseRequiredFields = {
id,
immutable,
updated_at,
updated_by: updatedByOrNull,
created_at,
created_by: createdByOrNull,
};
const responseOptionalFields = {
status: job_status,
status_date,
last_success_at,
last_success_message,
last_failure_at,
last_failure_message,
};
export const fullResponseSchema = t.intersection([
commonResponseParams,
responseTypeSpecific,
t.exact(t.type(responseRequiredFields)),
t.exact(t.partial(responseOptionalFields)),
]);
export type FullResponseSchema = t.TypeOf<typeof fullResponseSchema>;

View file

@ -8,11 +8,8 @@ import { updateRulesBulkSchema, UpdateRulesBulkSchema } from './update_rules_bul
import { exactCheck } from '../../../exact_check';
import { foldLeftRight } from '../../../test_utils';
import { formatErrors } from '../../../format_errors';
import {
getUpdateRulesSchemaMock,
getUpdateRulesSchemaDecodedMock,
} from './update_rules_schema.mock';
import { UpdateRulesSchema } from './update_rules_schema';
import { getUpdateRulesSchemaMock } from './rule_schemas.mock';
import { UpdateRulesSchema } from './rule_schemas';
// only the basics of testing are here.
// see: update_rules_schema.test.ts for the bulk of the validation tests
@ -34,13 +31,16 @@ describe('update_rules_bulk_schema', () => {
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "description"',
'Invalid value "undefined" supplied to "risk_score"',
'Invalid value "undefined" supplied to "name"',
'Invalid value "undefined" supplied to "severity"',
'Invalid value "undefined" supplied to "type"',
]);
expect(formatErrors(output.errors)).toContain(
'Invalid value "undefined" supplied to "description"'
);
expect(formatErrors(output.errors)).toContain(
'Invalid value "undefined" supplied to "risk_score"'
);
expect(formatErrors(output.errors)).toContain('Invalid value "undefined" supplied to "name"');
expect(formatErrors(output.errors)).toContain(
'Invalid value "undefined" supplied to "severity"'
);
expect(output.schema).toEqual({});
});
@ -51,7 +51,7 @@ describe('update_rules_bulk_schema', () => {
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([getUpdateRulesSchemaDecodedMock()]);
expect(output.schema).toEqual(payload);
});
test('two array elements do validate', () => {
@ -61,10 +61,7 @@ describe('update_rules_bulk_schema', () => {
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([
getUpdateRulesSchemaDecodedMock(),
getUpdateRulesSchemaDecodedMock(),
]);
expect(output.schema).toEqual(payload);
});
test('single array element with a missing value (risk_score) will not validate', () => {
@ -138,7 +135,7 @@ describe('update_rules_bulk_schema', () => {
madeUpValue: 'something',
};
const secondItem = getUpdateRulesSchemaMock();
const payload: UpdateRulesBulkSchema = [singleItem, secondItem];
const payload = [singleItem, secondItem];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
@ -180,28 +177,6 @@ describe('update_rules_bulk_schema', () => {
expect(output.schema).toEqual({});
});
test('The default for "from" will be "now-6m"', () => {
const { from, ...withoutFrom } = getUpdateRulesSchemaMock();
const payload: UpdateRulesBulkSchema = [withoutFrom];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect((output.schema as UpdateRulesBulkSchema)[0].from).toEqual('now-6m');
});
test('The default for "to" will be "now"', () => {
const { to, ...withoutTo } = getUpdateRulesSchemaMock();
const payload: UpdateRulesBulkSchema = [withoutTo];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect((output.schema as UpdateRulesBulkSchema)[0].to).toEqual('now');
});
test('You cannot set the severity to a value other than low, medium, high, or critical', () => {
const badSeverity = { ...getUpdateRulesSchemaMock(), severity: 'madeup' };
const payload = [badSeverity];
@ -222,9 +197,7 @@ describe('update_rules_bulk_schema', () => {
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([
{ ...getUpdateRulesSchemaDecodedMock(), note: '# test markdown' },
]);
expect(output.schema).toEqual(payload);
});
test('You can set "note" to an empty string', () => {
@ -234,10 +207,10 @@ describe('update_rules_bulk_schema', () => {
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([{ ...getUpdateRulesSchemaDecodedMock(), note: '' }]);
expect(output.schema).toEqual(payload);
});
test('You can set "note" to anything other than string', () => {
test('You cant set "note" to anything other than string', () => {
const payload = [
{
...getUpdateRulesSchemaMock(),
@ -255,26 +228,4 @@ describe('update_rules_bulk_schema', () => {
]);
expect(output.schema).toEqual({});
});
test('The default for "actions" will be an empty array', () => {
const { actions, ...withoutActions } = getUpdateRulesSchemaMock();
const payload: UpdateRulesBulkSchema = [withoutActions];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect((output.schema as UpdateRulesBulkSchema)[0].actions).toEqual([]);
});
test('The default for "throttle" will be null', () => {
const { throttle, ...withoutThrottle } = getUpdateRulesSchemaMock();
const payload: UpdateRulesBulkSchema = [withoutThrottle];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect((output.schema as UpdateRulesBulkSchema)[0].throttle).toEqual(null);
});
});

View file

@ -5,10 +5,7 @@
*/
import * as t from 'io-ts';
import { updateRulesSchema, UpdateRulesSchemaDecoded } from './update_rules_schema';
import { updateRulesSchema } from './rule_schemas';
export const updateRulesBulkSchema = t.array(updateRulesSchema);
export type UpdateRulesBulkSchema = t.TypeOf<typeof updateRulesBulkSchema>;
export type UpdateRulesBulkSchemaDecoded = UpdateRulesSchemaDecoded[];

View file

@ -1,45 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { UpdateRulesSchema, UpdateRulesSchemaDecoded } from './update_rules_schema';
import { DEFAULT_MAX_SIGNALS } from '../../../constants';
export const getUpdateRulesSchemaMock = (): UpdateRulesSchema => ({
description: 'some description',
name: 'Query with a rule id',
query: 'user.name: root or user.name: admin',
severity: 'high',
type: 'query',
risk_score: 55,
language: 'kuery',
rule_id: 'rule-1',
});
export const getUpdateRulesSchemaDecodedMock = (): UpdateRulesSchemaDecoded => ({
author: [],
description: 'some description',
name: 'Query with a rule id',
query: 'user.name: root or user.name: admin',
severity: 'high',
severity_mapping: [],
type: 'query',
risk_score: 55,
risk_score_mapping: [],
language: 'kuery',
references: [],
actions: [],
enabled: true,
false_positives: [],
from: 'now-6m',
interval: '5m',
max_signals: DEFAULT_MAX_SIGNALS,
tags: [],
to: 'now',
threat: [],
throttle: null,
exceptions_list: [],
rule_id: 'rule-1',
});

View file

@ -1,183 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import {
description,
anomaly_threshold,
filters,
RuleId,
index,
output_index,
saved_id,
timeline_id,
timeline_title,
meta,
machine_learning_job_id,
risk_score,
rule_id,
MaxSignals,
name,
severity,
Tags,
To,
type,
Threat,
threshold,
ThrottleOrNull,
note,
version,
References,
Actions,
Enabled,
FalsePositives,
From,
Interval,
language,
query,
id,
building_block_type,
license,
rule_name_override,
timestamp_override,
Author,
RiskScoreMapping,
SeverityMapping,
event_category_override,
} from '../common/schemas';
import {
threat_index,
concurrent_searches,
items_per_search,
threat_query,
threat_filters,
threat_mapping,
threat_language,
} from '../types/threat_mapping';
import {
DefaultStringArray,
DefaultActionsArray,
DefaultBooleanTrue,
DefaultFromString,
DefaultIntervalString,
DefaultMaxSignalsNumber,
DefaultToString,
DefaultThreatArray,
DefaultThrottleNull,
DefaultListArray,
ListArray,
DefaultRiskScoreMappingArray,
DefaultSeverityMappingArray,
} from '../types';
/**
* This almost identical to the create_rules_schema except for a few details.
* - The version will not be defaulted to a 1. If it is not given then its default will become the previous version auto-incremented
* This does break idempotency slightly as calls repeatedly without it will increment the number. If the version number is passed in
* this will update the rule's version number.
* - id is on here because you can pass in an id to update using it instead of rule_id.
*/
export const updateRulesSchema = t.intersection([
t.exact(
t.type({
description,
risk_score,
name,
severity,
type,
})
),
t.exact(
t.partial({
id, // defaults to "undefined" if not set during decode
actions: DefaultActionsArray, // defaults to empty actions array if not set during decode
anomaly_threshold, // defaults to undefined if not set during decode
author: DefaultStringArray, // defaults to empty array of strings if not set during decode
building_block_type, // defaults to undefined if not set during decode
enabled: DefaultBooleanTrue, // defaults to true if not set during decode
event_category_override,
false_positives: DefaultStringArray, // defaults to empty string array if not set during decode
filters, // defaults to undefined if not set during decode
from: DefaultFromString, // defaults to "now-6m" if not set during decode
rule_id, // defaults to "undefined" if not set during decode
index, // defaults to undefined if not set during decode
interval: DefaultIntervalString, // defaults to "5m" if not set during decode
query, // defaults to undefined if not set during decode
language, // defaults to undefined if not set during decode
license, // defaults to "undefined" if not set during decode
// TODO: output_index: This should be removed eventually
output_index, // defaults to "undefined" if not set during decode
saved_id, // defaults to "undefined" if not set during decode
timeline_id, // defaults to "undefined" if not set during decode
timeline_title, // defaults to "undefined" if not set during decode
meta, // defaults to "undefined" if not set during decode
machine_learning_job_id, // defaults to "undefined" if not set during decode
max_signals: DefaultMaxSignalsNumber, // defaults to DEFAULT_MAX_SIGNALS (100) if not set during decode
risk_score_mapping: DefaultRiskScoreMappingArray, // defaults to empty risk score mapping array if not set during decode
rule_name_override, // defaults to "undefined" if not set during decode
severity_mapping: DefaultSeverityMappingArray, // defaults to empty actions array if not set during decode
tags: DefaultStringArray, // defaults to empty string array if not set during decode
to: DefaultToString, // defaults to "now" if not set during decode
threat: DefaultThreatArray, // defaults to empty array if not set during decode
threshold, // defaults to "undefined" if not set during decode
throttle: DefaultThrottleNull, // defaults to "null" if not set during decode
timestamp_override, // defaults to "undefined" if not set during decode
references: DefaultStringArray, // defaults to empty array of strings if not set during decode
note, // defaults to "undefined" if not set during decode
version, // defaults to "undefined" if not set during decode
exceptions_list: DefaultListArray, // defaults to empty array if not set during decode
threat_mapping, // defaults to "undefined" if not set during decode
threat_query, // defaults to "undefined" if not set during decode
threat_filters, // defaults to "undefined" if not set during decode
threat_index, // defaults to "undefined" if not set during decode
threat_language, // defaults "undefined" if not set during decode
concurrent_searches, // defaults to "undefined" if not set during decode
items_per_search, // defaults to "undefined" if not set during decode
})
),
]);
export type UpdateRulesSchema = t.TypeOf<typeof updateRulesSchema>;
// This type is used after a decode since some things are defaults after a decode.
export type UpdateRulesSchemaDecoded = Omit<
UpdateRulesSchema,
| 'author'
| 'references'
| 'actions'
| 'enabled'
| 'false_positives'
| 'from'
| 'interval'
| 'max_signals'
| 'risk_score_mapping'
| 'severity_mapping'
| 'tags'
| 'to'
| 'threat'
| 'throttle'
| 'exceptions_list'
| 'rule_id'
> & {
author: Author;
references: References;
actions: Actions;
enabled: Enabled;
false_positives: FalsePositives;
from: From;
interval: Interval;
max_signals: MaxSignals;
risk_score_mapping: RiskScoreMapping;
severity_mapping: SeverityMapping;
tags: Tags;
to: To;
threat: Threat;
throttle: ThrottleOrNull;
exceptions_list: ListArray;
rule_id: RuleId;
};

View file

@ -4,28 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getUpdateRulesSchemaMock } from './update_rules_schema.mock';
import { UpdateRulesSchema } from './update_rules_schema';
import { getUpdateRulesSchemaMock } from './rule_schemas.mock';
import { UpdateRulesSchema } from './rule_schemas';
import { updateRuleValidateTypeDependents } from './update_rules_type_dependents';
describe('update_rules_type_dependents', () => {
test('saved_id is required when type is saved_query and will not validate without out', () => {
const schema: UpdateRulesSchema = { ...getUpdateRulesSchemaMock(), type: 'saved_query' };
delete schema.saved_id;
const errors = updateRuleValidateTypeDependents(schema);
expect(errors).toEqual(['when "type" is "saved_query", "saved_id" is required']);
});
test('saved_id is required when type is saved_query and validates with it', () => {
const schema: UpdateRulesSchema = {
...getUpdateRulesSchemaMock(),
type: 'saved_query',
saved_id: '123',
};
const errors = updateRuleValidateTypeDependents(schema);
expect(errors).toEqual([]);
});
test('You cannot omit timeline_title when timeline_id is present', () => {
const schema: UpdateRulesSchema = {
...getUpdateRulesSchemaMock(),
@ -85,26 +68,4 @@ describe('update_rules_type_dependents', () => {
const errors = updateRuleValidateTypeDependents(schema);
expect(errors).toEqual(['either "id" or "rule_id" must be set']);
});
test('threshold is required when type is threshold and validates with it', () => {
const schema: UpdateRulesSchema = {
...getUpdateRulesSchemaMock(),
type: 'threshold',
};
const errors = updateRuleValidateTypeDependents(schema);
expect(errors).toEqual(['when "type" is "threshold", "threshold" is required']);
});
test('threshold.value is required and has to be bigger than 0 when type is threshold and validates with it', () => {
const schema: UpdateRulesSchema = {
...getUpdateRulesSchemaMock(),
type: 'threshold',
threshold: {
field: '',
value: -1,
},
};
const errors = updateRuleValidateTypeDependents(schema);
expect(errors).toEqual(['"threshold.value" has to be bigger than 0']);
});
});

View file

@ -4,69 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { isMlRule } from '../../../machine_learning/helpers';
import { isThresholdRule } from '../../utils';
import { UpdateRulesSchema } from './update_rules_schema';
export const validateAnomalyThreshold = (rule: UpdateRulesSchema): string[] => {
if (isMlRule(rule.type)) {
if (rule.anomaly_threshold == null) {
return ['when "type" is "machine_learning" anomaly_threshold is required'];
} else {
return [];
}
} else {
return [];
}
};
export const validateQuery = (rule: UpdateRulesSchema): string[] => {
if (isMlRule(rule.type)) {
if (rule.query != null) {
return ['when "type" is "machine_learning", "query" cannot be set'];
} else {
return [];
}
} else {
return [];
}
};
export const validateLanguage = (rule: UpdateRulesSchema): string[] => {
if (isMlRule(rule.type)) {
if (rule.language != null) {
return ['when "type" is "machine_learning", "language" cannot be set'];
} else {
return [];
}
} else {
return [];
}
};
export const validateSavedId = (rule: UpdateRulesSchema): string[] => {
if (rule.type === 'saved_query') {
if (rule.saved_id == null) {
return ['when "type" is "saved_query", "saved_id" is required'];
} else {
return [];
}
} else {
return [];
}
};
export const validateMachineLearningJobId = (rule: UpdateRulesSchema): string[] => {
if (isMlRule(rule.type)) {
if (rule.machine_learning_job_id == null) {
return ['when "type" is "machine_learning", "machine_learning_job_id" is required'];
} else {
return [];
}
} else {
return [];
}
};
import { UpdateRulesSchema } from './rule_schemas';
export const validateTimelineId = (rule: UpdateRulesSchema): string[] => {
if (rule.timeline_id != null) {
@ -104,29 +42,6 @@ export const validateId = (rule: UpdateRulesSchema): string[] => {
}
};
export const validateThreshold = (rule: UpdateRulesSchema): string[] => {
if (isThresholdRule(rule.type)) {
if (!rule.threshold) {
return ['when "type" is "threshold", "threshold" is required'];
} else if (rule.threshold.value <= 0) {
return ['"threshold.value" has to be bigger than 0'];
} else {
return [];
}
}
return [];
};
export const updateRuleValidateTypeDependents = (schema: UpdateRulesSchema): string[] => {
return [
...validateId(schema),
...validateAnomalyThreshold(schema),
...validateQuery(schema),
...validateLanguage(schema),
...validateSavedId(schema),
...validateMachineLearningJobId(schema),
...validateTimelineId(schema),
...validateTimelineTitle(schema),
...validateThreshold(schema),
];
return [...validateId(schema), ...validateTimelineId(schema), ...validateTimelineTitle(schema)];
};

View file

@ -28,6 +28,7 @@ export * from './default_version_number';
export * from './iso_date_string';
export * from './lists';
export * from './lists_default_array';
export * from './non_empty_array';
export * from './non_empty_string';
export * from './only_false_allowed';
export * from './positive_integer';

View file

@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { NonEmptyArray } from './non_empty_array';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
const testSchema = t.keyof({
valid: true,
also_valid: true,
});
type TestSchema = t.TypeOf<typeof testSchema>;
const nonEmptyArraySchema = NonEmptyArray(testSchema, 'TestSchemaArray');
describe('non empty array', () => {
test('it should generate the correct name for non empty array', () => {
const newTestSchema = NonEmptyArray(testSchema);
expect(newTestSchema.name).toEqual('NonEmptyArray<"valid" | "also_valid">');
});
test('it should use a supplied name override', () => {
const newTestSchema = NonEmptyArray(testSchema, 'someName');
expect(newTestSchema.name).toEqual('someName');
});
test('it should NOT validate an empty array', () => {
const payload: string[] = [];
const decoded = nonEmptyArraySchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "[]" supplied to "TestSchemaArray"',
]);
expect(message.schema).toEqual({});
});
test('it should validate an array of testSchema', () => {
const payload: TestSchema[] = ['valid'];
const decoded = nonEmptyArraySchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of valid testSchema strings', () => {
const payload: TestSchema[] = ['valid', 'also_valid'];
const decoded = nonEmptyArraySchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate an array with a number', () => {
const payload = ['valid', 123];
const decoded = nonEmptyArraySchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "123" supplied to "TestSchemaArray"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate an array with an invalid string', () => {
const payload = ['valid', 'invalid'];
const decoded = nonEmptyArraySchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "invalid" supplied to "TestSchemaArray"',
]);
expect(message.schema).toEqual({});
});
test('it should not validate a null value', () => {
const payload = null;
const decoded = nonEmptyArraySchema.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "null" supplied to "TestSchemaArray"',
]);
expect(message.schema).toEqual({});
});
});

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
export const NonEmptyArray = <C extends t.Mixed>(
codec: C,
name: string = `NonEmptyArray<${codec.name}>`
) => {
const arrType = t.array(codec);
type ArrType = t.TypeOf<typeof arrType>;
return new t.Type<ArrType, ArrType, unknown>(
name,
arrType.is,
(input, context): Either<t.Errors, ArrType> => {
if (Array.isArray(input) && input.length === 0) {
return t.failure(input, context);
} else {
return arrType.validate(input, context);
}
},
t.identity
);
};

View file

@ -176,6 +176,19 @@ describe('threat_mapping', () => {
expect(message.schema).toEqual({});
});
test('it should fail validate with empty array', () => {
const payload: string[] = [];
const decoded = threat_mapping.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "[]" supplied to "NonEmptyArray<ThreatMap>"',
]);
expect(message.schema).toEqual({});
});
test('it should fail validation when concurrent_searches is < 0', () => {
const payload = -1;
const decoded = concurrent_searches.decode(payload);

View file

@ -8,6 +8,7 @@
import * as t from 'io-ts';
import { language } from '../common/schemas';
import { NonEmptyArray } from './non_empty_array';
import { NonEmptyString } from './non_empty_string';
import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero';
@ -41,7 +42,7 @@ export const threatMap = t.exact(
);
export type ThreatMap = t.TypeOf<typeof threatMap>;
export const threat_mapping = t.array(threatMap);
export const threat_mapping = NonEmptyArray(threatMap, 'NonEmptyArray<ThreatMap>');
export type ThreatMapping = t.TypeOf<typeof threat_mapping>;
export const threatMappingOrUndefined = t.union([threat_mapping, t.undefined]);

View file

@ -22,8 +22,10 @@ import {
getPrePackagedRulesStatus,
} from './api';
import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock';
import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock';
import {
getCreateRulesSchemaMock,
getUpdateRulesSchemaMock,
} from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock';
import { rulesMock } from './mock';
import { buildEsQuery } from 'src/plugins/data/common';
@ -64,7 +66,7 @@ describe('Detections Rules API', () => {
await updateRule({ rule: payload, signal: abortCtrl.signal });
expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', {
body:
'{"description":"some description","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","rule_id":"rule-1"}',
'{"description":"Detecting root and admin users","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd"}',
method: 'PUT',
signal: abortCtrl.signal,
});

View file

@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FullResponseSchema } from '../../../../../common/detection_engine/schemas/request';
import { HttpStart } from '../../../../../../../../src/core/public';
import {
DETECTION_ENGINE_RULES_URL,
@ -42,8 +43,8 @@ import { RulesSchema } from '../../../../../common/detection_engine/schemas/resp
*
* @throws An error if response is not OK
*/
export const createRule = async ({ rule, signal }: CreateRulesProps): Promise<RulesSchema> =>
KibanaServices.get().http.fetch<RulesSchema>(DETECTION_ENGINE_RULES_URL, {
export const createRule = async ({ rule, signal }: CreateRulesProps): Promise<FullResponseSchema> =>
KibanaServices.get().http.fetch<FullResponseSchema>(DETECTION_ENGINE_RULES_URL, {
method: 'POST',
body: JSON.stringify(rule),
signal,

View file

@ -7,7 +7,7 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useCreateRule, ReturnCreateRule } from './use_create_rule';
import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
jest.mock('./api');
@ -24,7 +24,7 @@ describe('useCreateRule', () => {
useCreateRule()
);
await waitForNextUpdate();
result.current[1](getUpdateRulesSchemaMock());
result.current[1](getCreateRulesSchemaMock());
rerender();
expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]);
});
@ -36,7 +36,7 @@ describe('useCreateRule', () => {
useCreateRule()
);
await waitForNextUpdate();
result.current[1](getUpdateRulesSchemaMock());
result.current[1](getCreateRulesSchemaMock());
await waitForNextUpdate();
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
});

View file

@ -7,7 +7,7 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useUpdateRule, ReturnUpdateRule } from './use_update_rule';
import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock';
import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
jest.mock('./api');

View file

@ -5,7 +5,7 @@
*/
import { List } from '../../../../../../common/detection_engine/schemas/types';
import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request/create_rules_schema';
import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request';
import { Rule } from '../../../../containers/detection_engine/rules';
import {
getListMock,

View file

@ -26,7 +26,7 @@ import { requestMock } from './request';
import { RuleNotificationAlertType } from '../../notifications/types';
import { QuerySignalsSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema';
import { SetSignalsStatusSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock';
import { EqlSearchResponse } from '../../../../../common/detection_engine/types';

View file

@ -18,7 +18,7 @@ import {
} from '../__mocks__/request_responses';
import { requestContextMock, serverMock, requestMock } from '../__mocks__';
import { createRulesBulkRoute } from './create_rules_bulk_route';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create());
@ -180,9 +180,7 @@ describe('create_rules_bulk', () => {
});
const result = server.validate(request);
expect(result.badRequest).toHaveBeenCalledWith(
'Invalid value "unexpected_type" supplied to "type"'
);
expect(result.badRequest).toHaveBeenCalled();
});
test('disallows invalid "from" param on rule', async () => {

View file

@ -6,18 +6,13 @@
import { validate } from '../../../../../common/validate';
import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents';
import { RuleAlertAction } from '../../../../../common/detection_engine/types';
import {
CreateRulesBulkSchemaDecoded,
createRulesBulkSchema,
} from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema';
import { createRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema';
import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema';
import { IRouter } from '../../../../../../../../src/core/server';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { SetupPlugins } from '../../../../plugin';
import { buildMlAuthz } from '../../../machine_learning/authz';
import { throwHttpError } from '../../../machine_learning/validation';
import { createRules } from '../../rules/create_rules';
import { readRules } from '../../rules/read_rules';
import { getDuplicates } from './utils';
import { transformValidateBulkError } from './validate';
@ -26,17 +21,14 @@ import { buildRouteValidation } from '../../../../utils/build_validation/route_v
import { transformBulkError, createBulkErrorObject, buildSiemResponse } from '../utils';
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
import { PartialFilter } from '../../types';
import { isMlRule } from '../../../../../common/machine_learning/helpers';
import { convertCreateAPIToInternalSchema } from '../../schemas/rule_converters';
export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
router.post(
{
path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`,
validate: {
body: buildRouteValidation<typeof createRulesBulkSchema, CreateRulesBulkSchemaDecoded>(
createRulesBulkSchema
),
body: buildRouteValidation(createRulesBulkSchema),
},
options: {
tags: ['access:securitySolution'],
@ -67,158 +59,63 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
ruleDefinitions
.filter((rule) => rule.rule_id == null || !dupes.includes(rule.rule_id))
.map(async (payloadRule) => {
const {
actions: actionsRest,
anomaly_threshold: anomalyThreshold,
author,
building_block_type: buildingBlockType,
description,
enabled,
event_category_override: eventCategoryOverride,
false_positives: falsePositives,
from,
query: queryOrUndefined,
language: languageOrUndefined,
license,
machine_learning_job_id: machineLearningJobId,
output_index: outputIndex,
saved_id: savedId,
meta,
filters: filtersRest,
rule_id: ruleId,
index,
interval,
max_signals: maxSignals,
risk_score: riskScore,
risk_score_mapping: riskScoreMapping,
rule_name_override: ruleNameOverride,
name,
severity,
severity_mapping: severityMapping,
tags,
threat,
threat_filters: threatFilters,
threat_index: threatIndex,
threat_mapping: threatMapping,
threat_query: threatQuery,
threat_language: threatLanguage,
concurrent_searches: concurrentSearches,
items_per_search: itemsPerSearch,
threshold,
throttle,
timestamp_override: timestampOverride,
to,
type,
references,
note,
timeline_id: timelineId,
timeline_title: timelineTitle,
version,
exceptions_list: exceptionsList,
} = payloadRule;
if (payloadRule.rule_id != null) {
const rule = await readRules({
alertsClient,
ruleId: payloadRule.rule_id,
id: undefined,
});
if (rule != null) {
return createBulkErrorObject({
ruleId: payloadRule.rule_id,
statusCode: 409,
message: `rule_id: "${payloadRule.rule_id}" already exists`,
});
}
}
const internalRule = convertCreateAPIToInternalSchema(payloadRule, siemClient);
try {
const validationErrors = createRuleValidateTypeDependents(payloadRule);
if (validationErrors.length) {
return createBulkErrorObject({
ruleId,
ruleId: internalRule.params.ruleId,
statusCode: 400,
message: validationErrors.join(),
});
}
const query = !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined;
const language =
!isMlRule(type) && languageOrUndefined == null ? 'kuery' : languageOrUndefined;
// TODO: Fix these either with an is conversion or by better typing them within io-ts
const actions: RuleAlertAction[] = actionsRest as RuleAlertAction[];
const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[];
throwHttpError(await mlAuthz.validateRuleType(type));
const finalIndex = outputIndex ?? siemClient.getSignalsIndex();
throwHttpError(await mlAuthz.validateRuleType(internalRule.params.type));
const finalIndex = internalRule.params.outputIndex;
const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex);
if (!indexExists) {
return createBulkErrorObject({
ruleId,
ruleId: internalRule.params.ruleId,
statusCode: 400,
message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`,
});
}
if (ruleId != null) {
const rule = await readRules({ alertsClient, ruleId, id: undefined });
if (rule != null) {
return createBulkErrorObject({
ruleId,
statusCode: 409,
message: `rule_id: "${ruleId}" already exists`,
});
}
}
const createdRule = await createRules({
alertsClient,
anomalyThreshold,
author,
buildingBlockType,
description,
enabled,
eventCategoryOverride,
falsePositives,
from,
immutable: false,
query,
language,
license,
machineLearningJobId,
outputIndex: finalIndex,
savedId,
timelineId,
timelineTitle,
meta,
filters,
ruleId,
index,
interval,
maxSignals,
name,
riskScore,
riskScoreMapping,
ruleNameOverride,
severity,
severityMapping,
tags,
to,
type,
threat,
threatFilters,
threatMapping,
threatQuery,
threatIndex,
threatLanguage,
concurrentSearches,
itemsPerSearch,
threshold,
timestampOverride,
references,
note,
version,
exceptionsList,
actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is set to rule, otherwise we are a notification and should not enable it,
const createdRule = await alertsClient.create({
data: internalRule,
});
const ruleActions = await updateRulesNotifications({
ruleAlertId: createdRule.id,
alertsClient,
savedObjectsClient,
enabled,
actions,
throttle,
name,
enabled: createdRule.enabled,
actions: payloadRule.actions,
throttle: payloadRule.throttle ?? null,
name: createdRule.name,
});
return transformValidateBulkError(ruleId, createdRule, ruleActions);
return transformValidateBulkError(
internalRule.params.ruleId,
createdRule,
ruleActions
);
} catch (err) {
return transformBulkError(ruleId, err);
return transformBulkError(internalRule.params.ruleId, err);
}
})
);

View file

@ -20,7 +20,7 @@ import { buildMlAuthz } from '../../../machine_learning/authz';
import { requestContextMock, serverMock, requestMock } from '../__mocks__';
import { createRulesRoute } from './create_rules_route';
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
jest.mock('../../rules/update_rules_notifications');
jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create());
@ -160,9 +160,7 @@ describe('create_rules', () => {
});
const result = server.validate(request);
expect(result.badRequest).toHaveBeenCalledWith(
'Invalid value "unexpected_type" supplied to "type"'
);
expect(result.badRequest).toHaveBeenCalled();
});
test('allows rule type of query and custom from and interval', async () => {

View file

@ -4,37 +4,28 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { isEqlRule } from '../../../../../common/detection_engine/utils';
import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents';
import { RuleAlertAction } from '../../../../../common/detection_engine/types';
import { buildRouteValidation } from '../../../../utils/build_validation/route_validation';
import {
createRulesSchema,
CreateRulesSchemaDecoded,
} from '../../../../../common/detection_engine/schemas/request/create_rules_schema';
import { IRouter } from '../../../../../../../../src/core/server';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { SetupPlugins } from '../../../../plugin';
import { buildMlAuthz } from '../../../machine_learning/authz';
import { throwHttpError } from '../../../machine_learning/validation';
import { createRules } from '../../rules/create_rules';
import { readRules } from '../../rules/read_rules';
import { transformValidate } from './validate';
import { getIndexExists } from '../../index/get_index_exists';
import { transformError, buildSiemResponse } from '../utils';
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
import { PartialFilter } from '../../types';
import { isMlRule } from '../../../../../common/machine_learning/helpers';
import { createRulesSchema } from '../../../../../common/detection_engine/schemas/request';
import { newTransformValidate } from './validate';
import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents';
import { convertCreateAPIToInternalSchema } from '../../schemas/rule_converters';
export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void => {
router.post(
{
path: DETECTION_ENGINE_RULES_URL,
validate: {
body: buildRouteValidation<typeof createRulesSchema, CreateRulesSchemaDecoded>(
createRulesSchema
),
body: buildRouteValidation(createRulesSchema),
},
options: {
tags: ['access:securitySolution'],
@ -46,66 +37,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void
if (validationErrors.length) {
return siemResponse.error({ statusCode: 400, body: validationErrors });
}
const {
actions: actionsRest,
anomaly_threshold: anomalyThreshold,
author,
building_block_type: buildingBlockType,
description,
enabled,
event_category_override: eventCategoryOverride,
false_positives: falsePositives,
from,
query: queryOrUndefined,
language: languageOrUndefined,
license,
output_index: outputIndex,
saved_id: savedId,
timeline_id: timelineId,
timeline_title: timelineTitle,
meta,
machine_learning_job_id: machineLearningJobId,
filters: filtersRest,
rule_id: ruleId,
index,
interval,
max_signals: maxSignals,
risk_score: riskScore,
risk_score_mapping: riskScoreMapping,
rule_name_override: ruleNameOverride,
name,
severity,
severity_mapping: severityMapping,
tags,
threat,
threshold,
threat_filters: threatFilters,
threat_index: threatIndex,
threat_query: threatQuery,
threat_mapping: threatMapping,
threat_language: threatLanguage,
concurrent_searches: concurrentSearches,
items_per_search: itemsPerSearch,
throttle,
timestamp_override: timestampOverride,
to,
type,
references,
note,
exceptions_list: exceptionsList,
} = request.body;
try {
const query = !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined;
const language =
!isMlRule(type) && !isEqlRule(type) && languageOrUndefined == null
? 'kuery'
: languageOrUndefined;
// TODO: Fix these either with an is conversion or by better typing them within io-ts
const actions: RuleAlertAction[] = actionsRest as RuleAlertAction[];
const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[];
const alertsClient = context.alerting?.getAlertsClient();
const clusterClient = context.core.elasticsearch.legacy.client;
const savedObjectsClient = context.core.savedObjects.client;
@ -115,93 +47,56 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void
return siemResponse.error({ statusCode: 404 });
}
if (request.body.rule_id != null) {
const rule = await readRules({
alertsClient,
ruleId: request.body.rule_id,
id: undefined,
});
if (rule != null) {
return siemResponse.error({
statusCode: 409,
body: `rule_id: "${request.body.rule_id}" already exists`,
});
}
}
const internalRule = convertCreateAPIToInternalSchema(request.body, siemClient);
const mlAuthz = buildMlAuthz({
license: context.licensing.license,
ml,
request,
savedObjectsClient,
});
throwHttpError(await mlAuthz.validateRuleType(type));
throwHttpError(await mlAuthz.validateRuleType(internalRule.params.type));
const finalIndex = outputIndex ?? siemClient.getSignalsIndex();
const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex);
const indexExists = await getIndexExists(
clusterClient.callAsCurrentUser,
internalRule.params.outputIndex
);
if (!indexExists) {
return siemResponse.error({
statusCode: 400,
body: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`,
body: `To create a rule, the index must exist first. Index ${internalRule.params.outputIndex} does not exist`,
});
}
if (ruleId != null) {
const rule = await readRules({ alertsClient, ruleId, id: undefined });
if (rule != null) {
return siemResponse.error({
statusCode: 409,
body: `rule_id: "${ruleId}" already exists`,
});
}
}
// This will create the endpoint list if it does not exist yet
await context.lists?.getExceptionListClient().createEndpointList();
const createdRule = await createRules({
alertsClient,
anomalyThreshold,
author,
buildingBlockType,
description,
enabled,
eventCategoryOverride,
falsePositives,
from,
immutable: false,
query,
language,
license,
outputIndex: finalIndex,
savedId,
timelineId,
timelineTitle,
meta,
machineLearningJobId,
filters,
ruleId,
index,
interval,
maxSignals,
name,
riskScore,
riskScoreMapping,
ruleNameOverride,
severity,
severityMapping,
tags,
to,
type,
threat,
threshold,
threatFilters,
threatIndex,
threatQuery,
threatMapping,
threatLanguage,
concurrentSearches,
itemsPerSearch,
timestampOverride,
references,
note,
version: 1,
exceptionsList,
actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is rule, otherwise we are a notification and should not enable it,
const createdRule = await alertsClient.create({
data: internalRule,
});
const ruleActions = await updateRulesNotifications({
ruleAlertId: createdRule.id,
alertsClient,
savedObjectsClient,
enabled,
actions,
throttle,
name,
enabled: createdRule.enabled,
actions: request.body.actions,
throttle: request.body.throttle ?? null,
name: createdRule.name,
});
const ruleStatuses = await ruleStatusSavedObjectsClientFactory(savedObjectsClient).find({
@ -211,7 +106,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void
search: `${createdRule.id}`,
searchFields: ['alertId'],
});
const [validated, errors] = transformValidate(
const [validated, errors] = newTransformValidate(
createdRule,
ruleActions,
ruleStatuses.saved_objects[0]

View file

@ -16,7 +16,7 @@ import {
} from '../__mocks__/request_responses';
import { serverMock, requestContextMock, requestMock } from '../__mocks__';
import { patchRulesBulkRoute } from './patch_rules_bulk_route';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create());
@ -60,6 +60,7 @@ describe('patch_rules_bulk', () => {
path: `${DETECTION_ENGINE_RULES_URL}/bulk_update`,
body: [
{
type: 'machine_learning',
rule_id: 'my-rule-id',
anomaly_threshold: 4,
machine_learning_job_id: 'some_job_id',

View file

@ -90,6 +90,7 @@ describe('patch_rules', () => {
method: 'patch',
path: DETECTION_ENGINE_RULES_URL,
body: {
type: 'machine_learning',
rule_id: 'my-rule-id',
anomaly_threshold: 4,
machine_learning_job_id: 'some_job_id',

View file

@ -18,7 +18,7 @@ import {
import { serverMock, requestContextMock, requestMock } from '../__mocks__';
import { updateRulesBulkRoute } from './update_rules_bulk_route';
import { BulkError } from '../utils';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create());
@ -150,9 +150,7 @@ describe('update_rules_bulk', () => {
});
const result = server.validate(request);
expect(result.badRequest).toHaveBeenCalledWith(
'Invalid value "unknown_type" supplied to "type"'
);
expect(result.badRequest).toHaveBeenCalled();
});
test('allows rule type of query and custom from and interval', async () => {

View file

@ -6,14 +6,9 @@
import { validate } from '../../../../../common/validate';
import { updateRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/update_rules_type_dependents';
import { RuleAlertAction } from '../../../../../common/detection_engine/types';
import { buildRouteValidation } from '../../../../utils/build_validation/route_validation';
import {
updateRulesBulkSchema,
UpdateRulesBulkSchemaDecoded,
} from '../../../../../common/detection_engine/schemas/request/update_rules_bulk_schema';
import { updateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/update_rules_bulk_schema';
import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema';
import { isMlRule } from '../../../../../common/machine_learning/helpers';
import { IRouter } from '../../../../../../../../src/core/server';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { SetupPlugins } from '../../../../plugin';
@ -25,16 +20,13 @@ import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '..
import { updateRules } from '../../rules/update_rules';
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
import { PartialFilter } from '../../types';
export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
router.put(
{
path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
validate: {
body: buildRouteValidation<typeof updateRulesBulkSchema, UpdateRulesBulkSchemaDecoded>(
updateRulesBulkSchema
),
body: buildRouteValidation(updateRulesBulkSchema),
},
options: {
tags: ['access:securitySolution'],
@ -61,139 +53,34 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
const rules = await Promise.all(
request.body.map(async (payloadRule) => {
const {
actions: actionsRest,
anomaly_threshold: anomalyThreshold,
author,
building_block_type: buildingBlockType,
description,
enabled,
event_category_override: eventCategoryOverride,
false_positives: falsePositives,
from,
query: queryOrUndefined,
language: languageOrUndefined,
license,
machine_learning_job_id: machineLearningJobId,
output_index: outputIndex,
saved_id: savedId,
timeline_id: timelineId,
timeline_title: timelineTitle,
meta,
filters: filtersRest,
rule_id: ruleId,
id,
index,
interval,
max_signals: maxSignals,
risk_score: riskScore,
risk_score_mapping: riskScoreMapping,
rule_name_override: ruleNameOverride,
name,
severity,
severity_mapping: severityMapping,
tags,
to,
type,
threat,
threshold,
threat_filters: threatFilters,
threat_index: threatIndex,
threat_query: threatQuery,
threat_mapping: threatMapping,
threat_language: threatLanguage,
concurrent_searches: concurrentSearches,
items_per_search: itemsPerSearch,
throttle,
timestamp_override: timestampOverride,
references,
note,
version,
exceptions_list: exceptionsList,
} = payloadRule;
const finalIndex = outputIndex ?? siemClient.getSignalsIndex();
const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)';
const idOrRuleIdOrUnknown = payloadRule.id ?? payloadRule.rule_id ?? '(unknown id)';
try {
const validationErrors = updateRuleValidateTypeDependents(payloadRule);
if (validationErrors.length) {
return createBulkErrorObject({
ruleId,
ruleId: payloadRule.rule_id,
statusCode: 400,
message: validationErrors.join(),
});
}
const query = !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined;
const language =
!isMlRule(type) && languageOrUndefined == null ? 'kuery' : languageOrUndefined;
// TODO: Fix these either with an is conversion or by better typing them within io-ts
const actions: RuleAlertAction[] = actionsRest as RuleAlertAction[];
const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[];
throwHttpError(await mlAuthz.validateRuleType(type));
throwHttpError(await mlAuthz.validateRuleType(payloadRule.type));
const rule = await updateRules({
alertsClient,
anomalyThreshold,
author,
buildingBlockType,
description,
enabled,
eventCategoryOverride,
falsePositives,
from,
query,
language,
license,
machineLearningJobId,
outputIndex: finalIndex,
savedId,
savedObjectsClient,
timelineId,
timelineTitle,
meta,
filters,
id,
ruleId,
index,
interval,
maxSignals,
riskScore,
riskScoreMapping,
ruleNameOverride,
name,
severity,
severityMapping,
tags,
to,
type,
threat,
threshold,
threatFilters,
threatIndex,
threatQuery,
threatMapping,
threatLanguage,
concurrentSearches,
itemsPerSearch,
timestampOverride,
references,
note,
version,
exceptionsList,
actions,
defaultOutputIndex: siemClient.getSignalsIndex(),
ruleUpdate: payloadRule,
});
if (rule != null) {
const ruleActions = await updateRulesNotifications({
ruleAlertId: rule.id,
alertsClient,
savedObjectsClient,
enabled,
actions,
throttle,
name,
enabled: payloadRule.enabled ?? true,
actions: payloadRule.actions,
throttle: payloadRule.throttle,
name: payloadRule.name,
});
const ruleStatuses = await ruleStatusClient.find({
perPage: 1,
@ -204,7 +91,7 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
});
return transformValidateBulkError(rule.id, rule, ruleActions, ruleStatuses);
} else {
return getIdBulkError({ id, ruleId });
return getIdBulkError({ id: payloadRule.id, ruleId: payloadRule.rule_id });
}
} catch (err) {
return transformBulkError(idOrRuleIdOrUnknown, err);

View file

@ -19,7 +19,7 @@ import { requestContextMock, serverMock, requestMock } from '../__mocks__';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
import { updateRulesRoute } from './update_rules_route';
import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock';
import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create());
jest.mock('../../rules/update_rules_notifications');
@ -131,7 +131,7 @@ describe('update_rules', () => {
path: DETECTION_ENGINE_RULES_URL,
body: {
...getUpdateRulesSchemaMock(),
rule_id: undefined,
id: undefined,
},
});
const response = await server.inject(noIdRequest, context);
@ -160,9 +160,7 @@ describe('update_rules', () => {
});
const result = await server.validate(request);
expect(result.badRequest).toHaveBeenCalledWith(
'Invalid value "unknown type" supplied to "type"'
);
expect(result.badRequest).toHaveBeenCalled();
});
test('allows rule type of query and custom from and interval', async () => {

View file

@ -4,13 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { updateRulesSchema } from '../../../../../common/detection_engine/schemas/request';
import { updateRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/update_rules_type_dependents';
import { RuleAlertAction } from '../../../../../common/detection_engine/types';
import {
updateRulesSchema,
UpdateRulesSchemaDecoded,
} from '../../../../../common/detection_engine/schemas/request/update_rules_schema';
import { isMlRule } from '../../../../../common/machine_learning/helpers';
import { IRouter } from '../../../../../../../../src/core/server';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { SetupPlugins } from '../../../../plugin';
@ -23,16 +18,13 @@ import { updateRules } from '../../rules/update_rules';
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
import { buildRouteValidation } from '../../../../utils/build_validation/route_validation';
import { PartialFilter } from '../../types';
export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
router.put(
{
path: DETECTION_ENGINE_RULES_URL,
validate: {
body: buildRouteValidation<typeof updateRulesSchema, UpdateRulesSchemaDecoded>(
updateRulesSchema
),
body: buildRouteValidation(updateRulesSchema),
},
options: {
tags: ['access:securitySolution'],
@ -44,67 +36,7 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
if (validationErrors.length) {
return siemResponse.error({ statusCode: 400, body: validationErrors });
}
const {
actions: actionsRest,
anomaly_threshold: anomalyThreshold,
author,
building_block_type: buildingBlockType,
description,
enabled,
event_category_override: eventCategoryOverride,
false_positives: falsePositives,
from,
query: queryOrUndefined,
language: languageOrUndefined,
license,
machine_learning_job_id: machineLearningJobId,
output_index: outputIndex,
saved_id: savedId,
timeline_id: timelineId,
timeline_title: timelineTitle,
meta,
filters: filtersRest,
rule_id: ruleId,
id,
index,
interval,
max_signals: maxSignals,
risk_score: riskScore,
risk_score_mapping: riskScoreMapping,
rule_name_override: ruleNameOverride,
name,
severity,
severity_mapping: severityMapping,
tags,
to,
type,
threat,
threshold,
threat_filters: threatFilters,
threat_index: threatIndex,
threat_query: threatQuery,
threat_mapping: threatMapping,
threat_language: threatLanguage,
concurrent_searches: concurrentSearches,
items_per_search: itemsPerSearch,
throttle,
timestamp_override: timestampOverride,
references,
note,
version,
exceptions_list: exceptionsList,
} = request.body;
try {
const query = !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined;
const language =
!isMlRule(type) && languageOrUndefined == null ? 'kuery' : languageOrUndefined;
// TODO: Fix these either with an is conversion or by better typing them within io-ts
const actions: RuleAlertAction[] = actionsRest as RuleAlertAction[];
const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[];
const alertsClient = context.alerting?.getAlertsClient();
const savedObjectsClient = context.core.savedObjects.client;
const siemClient = context.securitySolution?.getAppClient();
@ -120,59 +52,13 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
request,
savedObjectsClient,
});
throwHttpError(await mlAuthz.validateRuleType(type));
throwHttpError(await mlAuthz.validateRuleType(request.body.type));
const finalIndex = outputIndex ?? siemClient.getSignalsIndex();
const rule = await updateRules({
alertsClient,
anomalyThreshold,
author,
buildingBlockType,
description,
enabled,
eventCategoryOverride,
falsePositives,
from,
query,
language,
license,
machineLearningJobId,
outputIndex: finalIndex,
savedId,
savedObjectsClient,
timelineId,
timelineTitle,
meta,
filters,
id,
ruleId,
index,
interval,
maxSignals,
riskScore,
riskScoreMapping,
ruleNameOverride,
name,
severity,
severityMapping,
tags,
to,
type,
threat,
threshold,
threatFilters,
threatIndex,
threatQuery,
threatMapping,
threatLanguage,
concurrentSearches,
itemsPerSearch,
timestampOverride,
references,
note,
version,
exceptionsList,
actions: throttle === 'rule' ? actions : [], // Only enable actions if throttle is rule, otherwise we are a notification and should not enable it
defaultOutputIndex: siemClient.getSignalsIndex(),
ruleUpdate: request.body,
});
if (rule != null) {
@ -180,10 +66,10 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
ruleAlertId: rule.id,
alertsClient,
savedObjectsClient,
enabled,
actions,
throttle,
name,
enabled: request.body.enabled ?? true,
actions: request.body.actions,
throttle: request.body.throttle,
name: request.body.name,
});
const ruleStatuses = await ruleStatusClient.find({
perPage: 1,
@ -203,7 +89,7 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
return response.ok({ body: validated ?? {} });
}
} else {
const error = getIdError({ id, ruleId });
const error = getIdError({ id: request.body.id, ruleId: request.body.rule_id });
return siemResponse.error({
body: error.message,
statusCode: error.statusCode,

View file

@ -27,10 +27,10 @@ import { PartialAlert } from '../../../../../../alerts/server';
import { SanitizedAlert } from '../../../../../../alerts/server/types';
import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson';
import { RuleAlertType } from '../../rules/types';
import { CreateRulesBulkSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema';
import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types/threat_mapping';
import { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request';
type PromiseFromStreams = ImportRulesSchemaDecoded | Error;
@ -548,7 +548,7 @@ describe('utils', () => {
{ rule_id: 'value3' },
{},
{},
] as CreateRulesBulkSchemaDecoded,
] as CreateRulesBulkSchema,
'rule_id'
);
const expected = ['value2', 'value3'];
@ -562,7 +562,7 @@ describe('utils', () => {
{ rule_id: 'value3' },
{},
{},
] as CreateRulesBulkSchemaDecoded,
] as CreateRulesBulkSchema,
'rule_id'
);
const expected: string[] = [];

View file

@ -10,7 +10,7 @@ import uuid from 'uuid';
import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema';
import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema';
import { CreateRulesBulkSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema';
import { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema';
import { PartialAlert, FindResult } from '../../../../../../alerts/server';
import { INTERNAL_IDENTIFIER } from '../../../../../common/constants';
import {
@ -256,10 +256,7 @@ export const transformOrImportError = (
}
};
export const getDuplicates = (
ruleDefinitions: CreateRulesBulkSchemaDecoded,
by: 'rule_id'
): string[] => {
export const getDuplicates = (ruleDefinitions: CreateRulesBulkSchema, by: 'rule_id'): string[] => {
const mappedDuplicates = countBy(
by,
ruleDefinitions.filter((r) => r[by] != null)

View file

@ -9,6 +9,10 @@ import { fold } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import * as t from 'io-ts';
import {
FullResponseSchema,
fullResponseSchema,
} from '../../../../../common/detection_engine/schemas/request';
import { validate } from '../../../../../common/validate';
import { findRulesSchema } from '../../../../../common/detection_engine/schemas/response/find_rules_schema';
import {
@ -71,6 +75,19 @@ export const transformValidate = (
}
};
export const newTransformValidate = (
alert: PartialAlert,
ruleActions?: RuleActions | null,
ruleStatus?: SavedObject<IRuleSavedAttributesSavedObjectAttributes>
): [FullResponseSchema | null, string | null] => {
const transformed = transform(alert, ruleActions, ruleStatus);
if (transformed == null) {
return [null, 'Internal error transforming'];
} else {
return validate(transformed, fullResponseSchema);
}
};
export const transformValidateBulkError = (
ruleId: string,
alert: PartialAlert,

View file

@ -5,12 +5,22 @@
*/
import { defaults } from 'lodash/fp';
import { validate } from '../../../../common/validate';
import { PartialAlert } from '../../../../../alerts/server';
import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions';
import { PatchRulesOptions } from './types';
import { addTags } from './add_tags';
import { calculateVersion, calculateName, calculateInterval } from './utils';
import { calculateVersion, calculateName, calculateInterval, removeUndefined } from './utils';
import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client';
import { internalRuleUpdate } from '../schemas/rule_schemas';
class PatchError extends Error {
public readonly statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
}
}
export const patchRules = async ({
alertsClient,
@ -159,18 +169,24 @@ export const patchRules = async ({
}
);
const newRule = {
tags: addTags(tags ?? rule.tags, rule.params.ruleId, rule.params.immutable),
throttle: null,
name: calculateName({ updatedName: name, originalName: rule.name }),
schedule: {
interval: calculateInterval(interval, rule.schedule.interval),
},
actions: actions?.map(transformRuleToAlertAction) ?? rule.actions,
params: removeUndefined(nextParams),
};
const [validated, errors] = validate(newRule, internalRuleUpdate);
if (errors != null || validated === null) {
throw new PatchError(`Applying patch would create invalid rule: ${errors}`, 400);
}
const update = await alertsClient.update({
id: rule.id,
data: {
tags: addTags(tags ?? rule.tags, rule.params.ruleId, rule.params.immutable),
throttle: null,
name: calculateName({ updatedName: name, originalName: rule.name }),
schedule: {
interval: calculateInterval(interval, rule.schedule.interval),
},
actions: actions?.map(transformRuleToAlertAction) ?? rule.actions,
params: nextParams,
},
data: validated,
});
if (rule.enabled && enabled === false) {

View file

@ -13,6 +13,7 @@ import {
SavedObjectsFindResponse,
SavedObjectsClientContract,
} from 'kibana/server';
import { UpdateRulesSchema } from '../../../../common/detection_engine/schemas/request';
import { RuleAlertAction } from '../../../../common/detection_engine/types';
import {
FalsePositives,
@ -250,55 +251,10 @@ export interface CreateRulesOptions {
}
export interface UpdateRulesOptions {
id: IdOrUndefined;
savedObjectsClient: SavedObjectsClientContract;
alertsClient: AlertsClient;
anomalyThreshold: AnomalyThresholdOrUndefined;
author: Author;
buildingBlockType: BuildingBlockTypeOrUndefined;
description: Description;
enabled: Enabled;
eventCategoryOverride: EventCategoryOverrideOrUndefined;
falsePositives: FalsePositives;
from: From;
query: QueryOrUndefined;
language: LanguageOrUndefined;
savedId: SavedIdOrUndefined;
timelineId: TimelineIdOrUndefined;
timelineTitle: TimelineTitleOrUndefined;
meta: MetaOrUndefined;
machineLearningJobId: MachineLearningJobIdOrUndefined;
filters: PartialFilter[];
ruleId: RuleIdOrUndefined;
index: IndexOrUndefined;
interval: Interval;
license: LicenseOrUndefined;
maxSignals: MaxSignals;
riskScore: RiskScore;
riskScoreMapping: RiskScoreMapping;
ruleNameOverride: RuleNameOverrideOrUndefined;
outputIndex: OutputIndex;
name: Name;
severity: Severity;
severityMapping: SeverityMapping;
tags: Tags;
threat: Threat;
threshold: ThresholdOrUndefined;
threatFilters: ThreatFiltersOrUndefined;
threatIndex: ThreatIndexOrUndefined;
threatQuery: ThreatQueryOrUndefined;
threatMapping: ThreatMappingOrUndefined;
itemsPerSearch: ItemsPerSearchOrUndefined;
concurrentSearches: ConcurrentSearchesOrUndefined;
threatLanguage: ThreatLanguageOrUndefined;
timestampOverride: TimestampOverrideOrUndefined;
to: To;
type: Type;
references: References;
note: NoteOrUndefined;
version: VersionOrUndefined;
exceptionsList: ListArray;
actions: RuleAlertAction[];
defaultOutputIndex: string;
ruleUpdate: UpdateRulesSchema;
}
export interface PatchRulesOptions {

View file

@ -7,107 +7,21 @@
import { UpdateRulesOptions } from './types';
import { alertsClientMock } from '../../../../../alerts/server/mocks';
import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks';
import {
getUpdateRulesSchemaMock,
getUpdateMachineLearningSchemaMock,
} from '../../../../common/detection_engine/schemas/request/rule_schemas.mock';
export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({
author: ['Elastic'],
buildingBlockType: undefined,
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
alertsClient: alertsClientMock.create(),
savedObjectsClient: savedObjectsClientMock.create(),
anomalyThreshold: undefined,
description: 'some description',
enabled: true,
eventCategoryOverride: undefined,
falsePositives: ['false positive 1', 'false positive 2'],
from: 'now-6m',
query: 'user.name: root or user.name: admin',
language: 'kuery',
license: 'Elastic License',
savedId: 'savedId-123',
timelineId: 'timelineid-123',
timelineTitle: 'timeline-title-123',
meta: {},
machineLearningJobId: undefined,
filters: [],
ruleId: undefined,
index: ['index-123'],
interval: '5m',
maxSignals: 100,
riskScore: 80,
riskScoreMapping: [],
ruleNameOverride: undefined,
outputIndex: 'output-1',
name: 'Query with a rule id',
severity: 'high',
severityMapping: [],
tags: [],
threat: [],
threshold: undefined,
threatFilters: undefined,
threatIndex: undefined,
threatQuery: undefined,
threatMapping: undefined,
threatLanguage: undefined,
timestampOverride: undefined,
concurrentSearches: undefined,
itemsPerSearch: undefined,
to: 'now',
type: 'query',
references: ['http://www.example.com'],
note: '# sample markdown',
version: 1,
exceptionsList: [],
actions: [],
defaultOutputIndex: '.siem-signals-default',
ruleUpdate: getUpdateRulesSchemaMock(),
});
export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({
author: ['Elastic'],
buildingBlockType: undefined,
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
alertsClient: alertsClientMock.create(),
savedObjectsClient: savedObjectsClientMock.create(),
anomalyThreshold: 55,
description: 'some description',
enabled: true,
eventCategoryOverride: undefined,
falsePositives: ['false positive 1', 'false positive 2'],
from: 'now-6m',
query: undefined,
language: undefined,
license: 'Elastic License',
savedId: 'savedId-123',
timelineId: 'timelineid-123',
timelineTitle: 'timeline-title-123',
meta: {},
machineLearningJobId: 'new_job_id',
filters: [],
ruleId: undefined,
index: ['index-123'],
interval: '5m',
maxSignals: 100,
riskScore: 80,
riskScoreMapping: [],
ruleNameOverride: undefined,
outputIndex: 'output-1',
name: 'Machine Learning Job',
severity: 'high',
severityMapping: [],
tags: [],
threat: [],
threshold: undefined,
threatFilters: undefined,
threatIndex: undefined,
threatQuery: undefined,
threatMapping: undefined,
threatLanguage: undefined,
timestampOverride: undefined,
concurrentSearches: undefined,
itemsPerSearch: undefined,
to: 'now',
type: 'machine_learning',
references: ['http://www.example.com'],
note: '# sample markdown',
version: 1,
exceptionsList: [],
actions: [],
defaultOutputIndex: '.siem-signals-default',
ruleUpdate: getUpdateMachineLearningSchemaMock(),
});

View file

@ -12,61 +12,54 @@ import { AlertsClientMock } from '../../../../../alerts/server/alerts_client.moc
describe('updateRules', () => {
it('should call alertsClient.disable if the rule was enabled and enabled is false', async () => {
const rulesOptionsMock = getUpdateRulesOptionsMock();
const ruleOptions = {
...rulesOptionsMock,
enabled: false,
};
((ruleOptions.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue(getResult());
rulesOptionsMock.ruleUpdate.enabled = false;
((rulesOptionsMock.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue(
getResult()
);
await updateRules(ruleOptions);
await updateRules(rulesOptionsMock);
expect(ruleOptions.alertsClient.disable).toHaveBeenCalledWith(
expect(rulesOptionsMock.alertsClient.disable).toHaveBeenCalledWith(
expect.objectContaining({
id: rulesOptionsMock.id,
id: rulesOptionsMock.ruleUpdate.id,
})
);
});
it('should call alertsClient.enable if the rule was disabled and enabled is true', async () => {
const rulesOptionsMock = getUpdateRulesOptionsMock();
const ruleOptions = {
...rulesOptionsMock,
enabled: true,
};
rulesOptionsMock.ruleUpdate.enabled = true;
((ruleOptions.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue({
((rulesOptionsMock.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue({
...getResult(),
enabled: false,
});
await updateRules(ruleOptions);
await updateRules(rulesOptionsMock);
expect(ruleOptions.alertsClient.enable).toHaveBeenCalledWith(
expect(rulesOptionsMock.alertsClient.enable).toHaveBeenCalledWith(
expect.objectContaining({
id: rulesOptionsMock.id,
id: rulesOptionsMock.ruleUpdate.id,
})
);
});
it('calls the alertsClient with ML params', async () => {
it('calls the alertsClient with params', async () => {
const rulesOptionsMock = getUpdateMlRulesOptionsMock();
const ruleOptions = {
...rulesOptionsMock,
enabled: true,
};
rulesOptionsMock.ruleUpdate.enabled = true;
((ruleOptions.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue(
((rulesOptionsMock.alertsClient as unknown) as AlertsClientMock).get.mockResolvedValue(
getMlResult()
);
await updateRules(ruleOptions);
await updateRules(rulesOptionsMock);
expect(ruleOptions.alertsClient.update).toHaveBeenCalledWith(
expect(rulesOptionsMock.alertsClient.update).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
params: expect.objectContaining({
anomalyThreshold: rulesOptionsMock.anomalyThreshold,
machineLearningJobId: rulesOptionsMock.machineLearningJobId,
type: 'machine_learning',
severity: 'high',
}),
}),
})

View file

@ -4,179 +4,94 @@
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable complexity */
import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants';
import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions';
import { PartialAlert } from '../../../../../alerts/server';
import { readRules } from './read_rules';
import { UpdateRulesOptions } from './types';
import { addTags } from './add_tags';
import { calculateVersion } from './utils';
import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client';
import { typeSpecificSnakeToCamel } from '../schemas/rule_converters';
import { InternalRuleUpdate } from '../schemas/rule_schemas';
export const updateRules = async ({
alertsClient,
author,
buildingBlockType,
savedObjectsClient,
description,
eventCategoryOverride,
falsePositives,
enabled,
query,
language,
license,
outputIndex,
savedId,
timelineId,
timelineTitle,
meta,
filters,
from,
id,
ruleId,
index,
interval,
maxSignals,
riskScore,
riskScoreMapping,
ruleNameOverride,
name,
severity,
severityMapping,
tags,
threat,
threshold,
threatFilters,
threatIndex,
threatQuery,
threatMapping,
threatLanguage,
concurrentSearches,
itemsPerSearch,
timestampOverride,
to,
type,
references,
version,
note,
exceptionsList,
anomalyThreshold,
machineLearningJobId,
actions,
defaultOutputIndex,
ruleUpdate,
}: UpdateRulesOptions): Promise<PartialAlert | null> => {
const rule = await readRules({ alertsClient, ruleId, id });
if (rule == null) {
const existingRule = await readRules({
alertsClient,
ruleId: ruleUpdate.rule_id,
id: ruleUpdate.id,
});
if (existingRule == null) {
return null;
}
const calculatedVersion = calculateVersion(rule.params.immutable, rule.params.version, {
author,
buildingBlockType,
description,
eventCategoryOverride,
falsePositives,
query,
language,
license,
outputIndex,
savedId,
timelineId,
timelineTitle,
meta,
filters,
from,
index,
interval,
maxSignals,
riskScore,
riskScoreMapping,
ruleNameOverride,
name,
severity,
severityMapping,
tags,
threat,
threshold,
threatFilters,
threatIndex,
threatQuery,
threatMapping,
threatLanguage,
concurrentSearches,
itemsPerSearch,
timestampOverride,
to,
type,
references,
version,
note,
anomalyThreshold,
machineLearningJobId,
exceptionsList,
});
const typeSpecificParams = typeSpecificSnakeToCamel(ruleUpdate);
const throttle = ruleUpdate.throttle ?? null;
const enabled = ruleUpdate.enabled ?? true;
const newInternalRule: InternalRuleUpdate = {
name: ruleUpdate.name,
tags: addTags(ruleUpdate.tags ?? [], existingRule.params.ruleId, false),
params: {
author: ruleUpdate.author ?? [],
buildingBlockType: ruleUpdate.building_block_type,
description: ruleUpdate.description,
ruleId: existingRule.params.ruleId,
falsePositives: ruleUpdate.false_positives ?? [],
from: ruleUpdate.from ?? 'now-6m',
// Unlike the create route, immutable comes from the existing rule here
immutable: existingRule.params.immutable,
license: ruleUpdate.license,
outputIndex: ruleUpdate.output_index ?? defaultOutputIndex,
timelineId: ruleUpdate.timeline_id,
timelineTitle: ruleUpdate.timeline_title,
meta: ruleUpdate.meta,
maxSignals: ruleUpdate.max_signals ?? DEFAULT_MAX_SIGNALS,
riskScore: ruleUpdate.risk_score,
riskScoreMapping: ruleUpdate.risk_score_mapping ?? [],
ruleNameOverride: ruleUpdate.rule_name_override,
severity: ruleUpdate.severity,
severityMapping: ruleUpdate.severity_mapping ?? [],
threat: ruleUpdate.threat ?? [],
timestampOverride: ruleUpdate.timestamp_override,
to: ruleUpdate.to ?? 'now',
references: ruleUpdate.references ?? [],
note: ruleUpdate.note,
// Always use the version from the request if specified. If it isn't specified, leave immutable rules alone and
// increment the version of mutable rules by 1.
version:
ruleUpdate.version ?? existingRule.params.immutable
? existingRule.params.version
: existingRule.params.version + 1,
exceptionsList: ruleUpdate.exceptions_list ?? [],
...typeSpecificParams,
},
schedule: { interval: ruleUpdate.interval ?? '5m' },
actions: throttle === 'rule' ? (ruleUpdate.actions ?? []).map(transformRuleToAlertAction) : [],
throttle: null,
};
const update = await alertsClient.update({
id: rule.id,
data: {
tags: addTags(tags, rule.params.ruleId, rule.params.immutable),
name,
schedule: { interval },
actions: actions.map(transformRuleToAlertAction),
throttle: null,
params: {
author,
buildingBlockType,
description,
ruleId: rule.params.ruleId,
falsePositives,
from,
immutable: rule.params.immutable,
query,
language,
license,
outputIndex,
savedId,
timelineId,
timelineTitle,
meta,
filters,
index,
maxSignals,
riskScore,
riskScoreMapping,
ruleNameOverride,
severity,
severityMapping,
threat,
threshold,
threatFilters,
threatIndex,
threatQuery,
threatMapping,
threatLanguage,
timestampOverride,
to,
type,
references,
note,
version: calculatedVersion,
anomalyThreshold,
machineLearningJobId,
exceptionsList,
},
},
id: existingRule.id,
data: newInternalRule,
});
if (rule.enabled && enabled === false) {
await alertsClient.disable({ id: rule.id });
} else if (!rule.enabled && enabled === true) {
await alertsClient.enable({ id: rule.id });
if (existingRule.enabled && enabled === false) {
await alertsClient.disable({ id: existingRule.id });
} else if (!existingRule.enabled && enabled === true) {
await alertsClient.enable({ id: existingRule.id });
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
const ruleCurrentStatus = await ruleStatusClient.find({
perPage: 1,
sortField: 'statusDate',
sortOrder: 'desc',
search: rule.id,
search: existingRule.id,
searchFields: ['alertId'],
});
@ -189,6 +104,5 @@ export const updateRules = async ({
});
}
}
return { ...update, enabled };
};

View file

@ -136,10 +136,7 @@ export const calculateVersion = (
// the version number if only the enabled/disabled flag is being set. Likewise if we get other
// properties we are not expecting such as updatedAt we do not to cause a version number bump
// on that either.
const removedNullValues = pickBy<UpdateProperties>(
(value: unknown) => value != null,
updateProperties
);
const removedNullValues = removeUndefined(updateProperties);
if (isEmpty(removedNullValues)) {
return currentVersion;
} else {
@ -147,6 +144,10 @@ export const calculateVersion = (
}
};
export const removeUndefined = (obj: object) => {
return pickBy((value: unknown) => value != null, obj);
};
export const calculateName = ({
updatedName,
originalName,

View file

@ -0,0 +1,266 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import uuid from 'uuid';
import { InternalRuleCreate, InternalRuleResponse, TypeSpecificRuleParams } from './rule_schemas';
import { assertUnreachable } from '../../../../common/utility_types';
import {
CreateRulesSchema,
CreateTypeSpecific,
FullResponseSchema,
ResponseTypeSpecific,
} from '../../../../common/detection_engine/schemas/request';
import { RuleActions } from '../rule_actions/types';
import { AppClient } from '../../../types';
import { addTags } from '../rules/add_tags';
import { DEFAULT_MAX_SIGNALS, SERVER_APP_ID, SIGNALS_ID } from '../../../../common/constants';
import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions';
// These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema
// to the response API schema. This provides static type-check assurances that the internal schema is in sync with the API schema for
// required and defaultable fields. However, it is still possible to add an optional field to the API schema
// without causing a type-check error here.
// Converts params from the snake case API format to the internal camel case format AND applies default values where needed.
// Notice that params.language is possibly undefined for most rule types in the API but we default it to kuery to match
// the legacy API behavior
export const typeSpecificSnakeToCamel = (params: CreateTypeSpecific): TypeSpecificRuleParams => {
switch (params.type) {
case 'eql': {
return {
type: params.type,
language: params.language,
index: params.index,
query: params.query,
filters: params.filters,
eventCategoryOverride: params.event_category_override,
};
}
case 'threat_match': {
return {
type: params.type,
language: params.language ?? 'kuery',
index: params.index,
query: params.query,
filters: params.filters,
savedId: params.saved_id,
threatFilters: params.threat_filters,
threatQuery: params.threat_query,
threatMapping: params.threat_mapping,
threatLanguage: params.threat_language,
threatIndex: params.threat_index,
concurrentSearches: params.concurrent_searches,
itemsPerSearch: params.items_per_search,
};
}
case 'query': {
return {
type: params.type,
language: params.language ?? 'kuery',
index: params.index,
query: params.query ?? '',
filters: params.filters,
savedId: params.saved_id,
};
}
case 'saved_query': {
return {
type: params.type,
language: params.language ?? 'kuery',
index: params.index,
query: params.query,
filters: params.filters,
savedId: params.saved_id,
};
}
case 'threshold': {
return {
type: params.type,
language: params.language ?? 'kuery',
index: params.index,
query: params.query,
filters: params.filters,
savedId: params.saved_id,
threshold: params.threshold,
};
}
case 'machine_learning': {
return {
type: params.type,
anomalyThreshold: params.anomaly_threshold,
machineLearningJobId: params.machine_learning_job_id,
};
}
default: {
return assertUnreachable(params);
}
}
};
export const convertCreateAPIToInternalSchema = (
input: CreateRulesSchema,
siemClient: AppClient
): InternalRuleCreate => {
const typeSpecificParams = typeSpecificSnakeToCamel(input);
const newRuleId = input.rule_id ?? uuid.v4();
return {
name: input.name,
tags: addTags(input.tags ?? [], newRuleId, false),
alertTypeId: SIGNALS_ID,
consumer: SERVER_APP_ID,
params: {
author: input.author ?? [],
buildingBlockType: input.building_block_type,
description: input.description,
ruleId: newRuleId,
falsePositives: input.false_positives ?? [],
from: input.from ?? 'now-6m',
immutable: false,
license: input.license,
outputIndex: input.output_index ?? siemClient.getSignalsIndex(),
timelineId: input.timeline_id,
timelineTitle: input.timeline_title,
meta: input.meta,
maxSignals: input.max_signals ?? DEFAULT_MAX_SIGNALS,
riskScore: input.risk_score,
riskScoreMapping: input.risk_score_mapping ?? [],
ruleNameOverride: input.rule_name_override,
severity: input.severity,
severityMapping: input.severity_mapping ?? [],
threat: input.threat ?? [],
timestampOverride: input.timestamp_override,
to: input.to ?? 'now',
references: input.references ?? [],
note: input.note,
version: input.version ?? 1,
exceptionsList: input.exceptions_list ?? [],
...typeSpecificParams,
},
schedule: { interval: input.interval ?? '5m' },
enabled: input.enabled ?? true,
actions: input.throttle === 'rule' ? (input.actions ?? []).map(transformRuleToAlertAction) : [],
throttle: null,
};
};
// Converts the internal rule data structure to the response API schema
export const typeSpecificCamelToSnake = (params: TypeSpecificRuleParams): ResponseTypeSpecific => {
switch (params.type) {
case 'eql': {
return {
type: params.type,
language: params.language,
index: params.index,
query: params.query,
filters: params.filters,
event_category_override: params.eventCategoryOverride,
};
}
case 'threat_match': {
return {
type: params.type,
language: params.language,
index: params.index,
query: params.query,
filters: params.filters,
saved_id: params.savedId,
threat_filters: params.threatFilters,
threat_query: params.threatQuery,
threat_mapping: params.threatMapping,
threat_language: params.threatLanguage,
threat_index: params.threatIndex,
concurrent_searches: params.concurrentSearches,
items_per_search: params.itemsPerSearch,
};
}
case 'query': {
return {
type: params.type,
language: params.language,
index: params.index,
query: params.query,
filters: params.filters,
saved_id: params.savedId,
};
}
case 'saved_query': {
return {
type: params.type,
language: params.language,
index: params.index,
query: params.query,
filters: params.filters,
saved_id: params.savedId,
};
}
case 'threshold': {
return {
type: params.type,
language: params.language,
index: params.index,
query: params.query,
filters: params.filters,
saved_id: params.savedId,
threshold: params.threshold,
};
}
case 'machine_learning': {
return {
type: params.type,
anomaly_threshold: params.anomalyThreshold,
machine_learning_job_id: params.machineLearningJobId,
};
}
default: {
return assertUnreachable(params);
}
}
};
export const internalRuleToAPIResponse = (
rule: InternalRuleResponse,
ruleActions: RuleActions
): FullResponseSchema => {
return {
id: rule.id,
immutable: rule.params.immutable,
updated_at: rule.updatedAt,
updated_by: rule.updatedBy,
created_at: rule.createdAt,
created_by: rule.createdBy,
name: rule.name,
tags: rule.tags,
interval: rule.schedule.interval,
enabled: rule.enabled,
throttle: ruleActions.ruleThrottle,
actions: ruleActions.actions,
description: rule.params.description,
risk_score: rule.params.riskScore,
severity: rule.params.severity,
building_block_type: rule.params.buildingBlockType,
note: rule.params.note,
license: rule.params.license,
output_index: rule.params.outputIndex,
timeline_id: rule.params.timelineId,
timeline_title: rule.params.timelineTitle,
meta: rule.params.meta,
rule_name_override: rule.params.ruleNameOverride,
timestamp_override: rule.params.timestampOverride,
author: rule.params.author ?? [],
false_positives: rule.params.falsePositives,
from: rule.params.from,
rule_id: rule.params.ruleId,
max_signals: rule.params.maxSignals,
risk_score_mapping: rule.params.riskScoreMapping ?? [],
severity_mapping: rule.params.severityMapping ?? [],
threat: rule.params.threat,
to: rule.params.to,
references: rule.params.references,
version: rule.params.version,
exceptions_list: rule.params.exceptionsList ?? [],
...typeSpecificCamelToSnake(rule.params),
};
};

View file

@ -0,0 +1,210 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { listArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists';
import {
threat_mapping,
threat_index,
threat_query,
concurrentSearchesOrUndefined,
itemsPerSearchOrUndefined,
} from '../../../../common/detection_engine/schemas/types/threat_mapping';
import {
authorOrUndefined,
buildingBlockTypeOrUndefined,
description,
enabled,
noteOrUndefined,
false_positives,
from,
rule_id,
immutable,
indexOrUndefined,
licenseOrUndefined,
output_index,
timelineIdOrUndefined,
timelineTitleOrUndefined,
metaOrUndefined,
name,
query,
queryOrUndefined,
filtersOrUndefined,
machine_learning_job_id,
max_signals,
risk_score,
riskScoreMappingOrUndefined,
ruleNameOverrideOrUndefined,
severity,
severityMappingOrUndefined,
tags,
timestampOverrideOrUndefined,
threat,
to,
references,
version,
eventCategoryOverrideOrUndefined,
savedIdOrUndefined,
saved_id,
threshold,
anomaly_threshold,
actionsCamel,
throttleOrNull,
createdByOrNull,
updatedByOrNull,
created_at,
updated_at,
} from '../../../../common/detection_engine/schemas/common/schemas';
import { SIGNALS_ID, SERVER_APP_ID } from '../../../../common/constants';
const nonEqlLanguages = t.keyof({ kuery: null, lucene: null });
export const baseRuleParams = t.exact(
t.type({
author: authorOrUndefined,
buildingBlockType: buildingBlockTypeOrUndefined,
description,
note: noteOrUndefined,
falsePositives: false_positives,
from,
ruleId: rule_id,
immutable,
license: licenseOrUndefined,
outputIndex: output_index,
timelineId: timelineIdOrUndefined,
timelineTitle: timelineTitleOrUndefined,
meta: metaOrUndefined,
// maxSignals not used in ML rules but probably should be used
maxSignals: max_signals,
riskScore: risk_score,
riskScoreMapping: riskScoreMappingOrUndefined,
ruleNameOverride: ruleNameOverrideOrUndefined,
severity,
severityMapping: severityMappingOrUndefined,
timestampOverride: timestampOverrideOrUndefined,
threat,
to,
references,
version,
exceptionsList: listArrayOrUndefined,
})
);
export type BaseRuleParams = t.TypeOf<typeof baseRuleParams>;
const eqlSpecificRuleParams = t.type({
type: t.literal('eql'),
language: t.literal('eql'),
index: indexOrUndefined,
query,
filters: filtersOrUndefined,
eventCategoryOverride: eventCategoryOverrideOrUndefined,
});
const threatSpecificRuleParams = t.type({
type: t.literal('threat_match'),
language: nonEqlLanguages,
index: indexOrUndefined,
query,
filters: filtersOrUndefined,
savedId: savedIdOrUndefined,
threatFilters: filtersOrUndefined,
threatQuery: threat_query,
threatMapping: threat_mapping,
threatLanguage: t.union([nonEqlLanguages, t.undefined]),
threatIndex: threat_index,
concurrentSearches: concurrentSearchesOrUndefined,
itemsPerSearch: itemsPerSearchOrUndefined,
});
const querySpecificRuleParams = t.exact(
t.type({
type: t.literal('query'),
language: nonEqlLanguages,
index: indexOrUndefined,
query,
filters: filtersOrUndefined,
savedId: savedIdOrUndefined,
})
);
const savedQuerySpecificRuleParams = t.type({
type: t.literal('saved_query'),
// Having language, query, and filters possibly defined adds more code confusion and probably user confusion
// if the saved object gets deleted for some reason
language: nonEqlLanguages,
index: indexOrUndefined,
query: queryOrUndefined,
filters: filtersOrUndefined,
savedId: saved_id,
});
const thresholdSpecificRuleParams = t.type({
type: t.literal('threshold'),
language: nonEqlLanguages,
index: indexOrUndefined,
query,
filters: filtersOrUndefined,
savedId: savedIdOrUndefined,
threshold,
});
const machineLearningSpecificRuleParams = t.type({
type: t.literal('machine_learning'),
anomalyThreshold: anomaly_threshold,
machineLearningJobId: machine_learning_job_id,
});
export const typeSpecificRuleParams = t.union([
eqlSpecificRuleParams,
threatSpecificRuleParams,
querySpecificRuleParams,
savedQuerySpecificRuleParams,
thresholdSpecificRuleParams,
machineLearningSpecificRuleParams,
]);
export type TypeSpecificRuleParams = t.TypeOf<typeof typeSpecificRuleParams>;
export const ruleParams = t.intersection([baseRuleParams, typeSpecificRuleParams]);
export type RuleParams = t.TypeOf<typeof ruleParams>;
export const internalRuleCreate = t.type({
name,
tags,
alertTypeId: t.literal(SIGNALS_ID),
consumer: t.literal(SERVER_APP_ID),
schedule: t.type({
interval: t.string,
}),
enabled,
actions: actionsCamel,
params: ruleParams,
throttle: throttleOrNull,
});
export type InternalRuleCreate = t.TypeOf<typeof internalRuleCreate>;
export const internalRuleUpdate = t.type({
name,
tags,
schedule: t.type({
interval: t.string,
}),
actions: actionsCamel,
params: ruleParams,
throttle: throttleOrNull,
});
export type InternalRuleUpdate = t.TypeOf<typeof internalRuleUpdate>;
export const internalRuleResponse = t.intersection([
internalRuleCreate,
t.type({
id: t.string,
createdBy: createdByOrNull,
updatedBy: updatedByOrNull,
createdAt: created_at,
updatedAt: updated_at,
}),
]);
export type InternalRuleResponse = t.TypeOf<typeof internalRuleResponse>;

View file

@ -5,6 +5,7 @@
*/
import expect from '@kbn/expect';
import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request';
import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@ -65,13 +66,51 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should create a single rule without an input index', async () => {
const { index, ...payload } = getSimpleRule();
const { index: _index, ...expected } = getSimpleRuleOutput();
const rule: CreateRulesSchema = {
name: 'Simple Rule Query',
description: 'Simple Rule Query',
enabled: true,
risk_score: 1,
rule_id: 'rule-1',
severity: 'high',
type: 'query',
query: 'user.name: root or user.name: admin',
};
const expected = {
actions: [],
author: [],
created_by: 'elastic',
description: 'Simple Rule Query',
enabled: true,
false_positives: [],
from: 'now-6m',
immutable: false,
interval: '5m',
rule_id: 'rule-1',
language: 'kuery',
output_index: '.siem-signals-default',
max_signals: 100,
risk_score: 1,
risk_score_mapping: [],
name: 'Simple Rule Query',
query: 'user.name: root or user.name: admin',
references: [],
severity: 'high',
severity_mapping: [],
updated_by: 'elastic',
tags: [],
to: 'now',
type: 'query',
threat: [],
throttle: 'no_actions',
exceptions_list: [],
version: 1,
};
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(payload)
.send(rule)
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);

View file

@ -19,6 +19,7 @@ import {
getSimpleRuleUpdate,
getSimpleMlRuleUpdate,
createRule,
getSimpleRule,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
@ -38,7 +39,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update a single rule property of name using a rule_id', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
// update a simple rule's name
const updatedRule = getSimpleRuleUpdate('rule-1');
@ -60,7 +61,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should return a 403 forbidden if it is a machine learning job', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
// update a simple rule's type to try to be a machine learning job type
const updatedRule = getSimpleMlRuleUpdate('rule-1');
@ -81,7 +82,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update a single rule property of name using an auto-generated rule_id', async () => {
const rule = getSimpleRuleUpdate('rule-1');
const rule = getSimpleRule('rule-1');
delete rule.rule_id;
const createRuleBody = await createRule(supertest, rule);
@ -105,7 +106,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update a single rule property of name using the auto-generated id', async () => {
const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1'));
const createdBody = await createRule(supertest, getSimpleRule('rule-1'));
// update a simple rule's name
const updatedRule = getSimpleRuleUpdate('rule-1');
@ -127,7 +128,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should change the version of a rule when it updates enabled and another property', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
// update a simple rule's enabled to false and another property
const updatedRule = getSimpleRuleUpdate('rule-1');
@ -150,7 +151,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
const ruleUpdate = getSimpleRuleUpdate('rule-1');
ruleUpdate.timeline_title = 'some title';

View file

@ -18,6 +18,7 @@ import {
removeServerGeneratedPropertiesIncludingRuleId,
getSimpleRuleUpdate,
createRule,
getSimpleRule,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
@ -37,7 +38,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update a single rule property of name using a rule_id', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
const updatedRule = getSimpleRuleUpdate('rule-1');
updatedRule.name = 'some other name';
@ -57,7 +58,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update two rule properties of name using the two rules rule_id', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
// create a second simple rule
await supertest
@ -94,7 +95,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update a single rule property of name using an id', async () => {
const createRuleBody = await createRule(supertest, getSimpleRuleUpdate('rule-1'));
const createRuleBody = await createRule(supertest, getSimpleRule('rule-1'));
// update a simple rule's name
const updatedRule1 = getSimpleRuleUpdate('rule-1');
@ -116,8 +117,8 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update two rule properties of name using the two rules id', async () => {
const createRule1 = await createRule(supertest, getSimpleRuleUpdate('rule-1'));
const createRule2 = await createRule(supertest, getSimpleRuleUpdate('rule-2'));
const createRule1 = await createRule(supertest, getSimpleRule('rule-1'));
const createRule2 = await createRule(supertest, getSimpleRule('rule-2'));
// update both rule names
const updatedRule1 = getSimpleRuleUpdate('rule-1');
@ -151,7 +152,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update a single rule property of name using the auto-generated id', async () => {
const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1'));
const createdBody = await createRule(supertest, getSimpleRule('rule-1'));
// update a simple rule's name
const updatedRule1 = getSimpleRuleUpdate('rule-1');
@ -173,7 +174,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should change the version of a rule when it updates enabled and another property', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
// update a simple rule's enabled to false and another property
const updatedRule1 = getSimpleRuleUpdate('rule-1');
@ -196,7 +197,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
// update a simple rule's timeline_title
const ruleUpdate = getSimpleRuleUpdate('rule-1');
@ -269,7 +270,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update one rule property and give an error about a second fake rule_id', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
const ruleUpdate = getSimpleRuleUpdate('rule-1');
ruleUpdate.name = 'some other name';
@ -304,7 +305,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update one rule property and give an error about a second fake id', async () => {
const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1'));
const createdBody = await createRule(supertest, getSimpleRule('rule-1'));
// update one rule name and give a fake id for the second
const rule1 = getSimpleRuleUpdate();

View file

@ -6,6 +6,7 @@
import expect from '@kbn/expect';
import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request';
import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
@ -19,7 +20,6 @@ import {
waitForRuleSuccess,
createRule,
} from '../../utils';
import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext) => {

View file

@ -7,10 +7,10 @@
/* eslint-disable @typescript-eslint/naming-convention */
import expect from '@kbn/expect';
import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request';
import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock';
import { deleteAllExceptions } from '../../../lists_api_integration/utils';
import { RulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/response';
import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request';
import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock';
import { CreateExceptionListItemSchema } from '../../../../plugins/lists/common';
import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants';
@ -422,7 +422,14 @@ export default ({ getService }: FtrProviderContext) => {
await createExceptionListItem(supertest, exceptionListItem);
const ruleWithException: CreateRulesSchema = {
...getSimpleRule(),
name: 'Simple Rule Query',
description: 'Simple Rule Query',
enabled: true,
risk_score: 1,
rule_id: 'rule-1',
severity: 'high',
index: ['auditbeat-*'],
type: 'query',
from: '1900-01-01T00:00:00.000Z',
query: 'host.name: "suricata-sensor-amsterdam"',
exceptions_list: [
@ -460,9 +467,16 @@ export default ({ getService }: FtrProviderContext) => {
await createExceptionListItem(supertest, exceptionListItem);
const ruleWithException: CreateRulesSchema = {
...getSimpleRule(),
name: 'Simple Rule Query',
description: 'Simple Rule Query',
enabled: true,
risk_score: 1,
rule_id: 'rule-1',
severity: 'high',
index: ['auditbeat-*'],
type: 'query',
from: '1900-01-01T00:00:00.000Z',
query: 'host.name: "suricata-sensor-amsterdam"', // this matches all the exceptions we should exclude
query: 'host.name: "suricata-sensor-amsterdam"',
exceptions_list: [
{
id,

View file

@ -5,6 +5,7 @@
*/
import expect from '@kbn/expect';
import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request';
import {
DETECTION_ENGINE_RULES_URL,
@ -110,13 +111,51 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should create a single rule without an input index', async () => {
const { index, ...payload } = getSimpleRule();
const { index: _index, ...expected } = getSimpleRuleOutput();
const rule: CreateRulesSchema = {
name: 'Simple Rule Query',
description: 'Simple Rule Query',
enabled: true,
risk_score: 1,
rule_id: 'rule-1',
severity: 'high',
type: 'query',
query: 'user.name: root or user.name: admin',
};
const expected = {
actions: [],
author: [],
created_by: 'elastic',
description: 'Simple Rule Query',
enabled: true,
false_positives: [],
from: 'now-6m',
immutable: false,
interval: '5m',
rule_id: 'rule-1',
language: 'kuery',
output_index: '.siem-signals-default',
max_signals: 100,
risk_score: 1,
risk_score_mapping: [],
name: 'Simple Rule Query',
query: 'user.name: root or user.name: admin',
references: [],
severity: 'high',
severity_mapping: [],
updated_by: 'elastic',
tags: [],
to: 'now',
type: 'query',
threat: [],
throttle: 'no_actions',
exceptions_list: [],
version: 1,
};
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(payload)
.send(rule)
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);

View file

@ -23,7 +23,7 @@ import {
waitForSignalsToBePresent,
} from '../../utils';
import { getCreateThreatMatchRulesSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock';
import { getCreateThreatMatchRulesSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock';
import { getThreatMatchingSchemaPartialMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks';
// eslint-disable-next-line import/no-default-export
@ -99,7 +99,13 @@ export default ({ getService }: FtrProviderContext) => {
it('should be able to execute and get 10 signals when doing a specific query', async () => {
const rule: CreateRulesSchema = {
...getCreateThreatMatchRulesSchemaMock(),
description: 'Detecting root and admin users',
name: 'Query with a rule id',
severity: 'high',
type: 'threat_match',
risk_score: 55,
language: 'kuery',
rule_id: 'rule-1',
from: '1900-01-01T00:00:00.000Z',
query: '*:*',
threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip
@ -127,7 +133,13 @@ export default ({ getService }: FtrProviderContext) => {
it('should return 0 matches if the mapping does not match against anything in the mapping', async () => {
const rule: CreateRulesSchema = {
...getCreateThreatMatchRulesSchemaMock(),
description: 'Detecting root and admin users',
name: 'Query with a rule id',
severity: 'high',
type: 'threat_match',
risk_score: 55,
language: 'kuery',
rule_id: 'rule-1',
from: '1900-01-01T00:00:00.000Z',
query: '*:*',
threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip
@ -155,7 +167,13 @@ export default ({ getService }: FtrProviderContext) => {
it('should return 0 signals when using an AND and one of the clauses does not have data', async () => {
const rule: CreateRulesSchema = {
...getCreateThreatMatchRulesSchemaMock(),
description: 'Detecting root and admin users',
name: 'Query with a rule id',
severity: 'high',
type: 'threat_match',
risk_score: 55,
language: 'kuery',
rule_id: 'rule-1',
from: '1900-01-01T00:00:00.000Z',
query: '*:*',
threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip
@ -187,7 +205,13 @@ export default ({ getService }: FtrProviderContext) => {
it('should return 0 signals when using an AND and one of the clauses has a made up value that does not exist', async () => {
const rule: CreateRulesSchema = {
...getCreateThreatMatchRulesSchemaMock(),
description: 'Detecting root and admin users',
name: 'Query with a rule id',
severity: 'high',
type: 'threat_match',
risk_score: 55,
language: 'kuery',
rule_id: 'rule-1',
from: '1900-01-01T00:00:00.000Z',
query: '*:*',
threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip

View file

@ -6,7 +6,7 @@
import expect from '@kbn/expect';
import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request';
import { QueryCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request';
import { DEFAULT_SIGNALS_INDEX } from '../../../../plugins/security_solution/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
@ -53,7 +53,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should have the specific audit record for _id or none of these tests below will pass', async () => {
const rule: CreateRulesSchema = {
const rule: QueryCreateSchema = {
...getSimpleRule(),
from: '1900-01-01T00:00:00.000Z',
query: `_id:${ID}`,
@ -65,7 +65,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should have recorded the rule_id within the signal', async () => {
const rule: CreateRulesSchema = {
const rule: QueryCreateSchema = {
...getSimpleRule(),
from: '1900-01-01T00:00:00.000Z',
query: `_id:${ID}`,
@ -77,7 +77,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should query and get back expected signal structure using a basic KQL query', async () => {
const rule: CreateRulesSchema = {
const rule: QueryCreateSchema = {
...getSimpleRule(),
from: '1900-01-01T00:00:00.000Z',
query: `_id:${ID}`,
@ -124,7 +124,7 @@ export default ({ getService }: FtrProviderContext) => {
it('should query and get back expected signal structure when it is a signal on a signal', async () => {
// create a 1 signal from 1 auditbeat record
const rule: CreateRulesSchema = {
const rule: QueryCreateSchema = {
...getSimpleRule(),
from: '1900-01-01T00:00:00.000Z',
query: `_id:${ID}`,
@ -133,7 +133,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForSignalsToBePresent(supertest, 1);
// Run signals on top of that 1 signal which should create a single signal (on top of) a signal
const ruleForSignals: CreateRulesSchema = {
const ruleForSignals: QueryCreateSchema = {
...getSimpleRule(),
rule_id: 'signal-on-signal',
index: [`${DEFAULT_SIGNALS_INDEX}*`],
@ -209,7 +209,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should have the specific audit record for _id or none of these tests below will pass', async () => {
const rule: CreateRulesSchema = {
const rule: QueryCreateSchema = {
...getSimpleRule(),
index: ['signal_name_clash'],
from: '1900-01-01T00:00:00.000Z',
@ -222,7 +222,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should have recorded the rule_id within the signal', async () => {
const rule: CreateRulesSchema = {
const rule: QueryCreateSchema = {
...getSimpleRule(),
index: ['signal_name_clash'],
from: '1900-01-01T00:00:00.000Z',
@ -235,7 +235,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should query and get back expected signal structure using a basic KQL query', async () => {
const rule: CreateRulesSchema = {
const rule: QueryCreateSchema = {
...getSimpleRule(),
index: ['signal_name_clash'],
from: '1900-01-01T00:00:00.000Z',
@ -278,7 +278,7 @@ export default ({ getService }: FtrProviderContext) => {
it('should query and get back expected signal structure when it is a signal on a signal', async () => {
// create a 1 signal from 1 auditbeat record
const rule: CreateRulesSchema = {
const rule: QueryCreateSchema = {
...getSimpleRule(),
index: ['signal_name_clash'],
from: '1900-01-01T00:00:00.000Z',
@ -288,7 +288,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForSignalsToBePresent(supertest, 1);
// Run signals on top of that 1 signal which should create a single signal (on top of) a signal
const ruleForSignals: CreateRulesSchema = {
const ruleForSignals: QueryCreateSchema = {
...getSimpleRule(),
rule_id: 'signal-on-signal',
index: [`${DEFAULT_SIGNALS_INDEX}*`],
@ -362,7 +362,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should have the specific audit record for _id or none of these tests below will pass', async () => {
const rule: CreateRulesSchema = {
const rule: QueryCreateSchema = {
...getSimpleRule(),
index: ['signal_object_clash'],
from: '1900-01-01T00:00:00.000Z',
@ -375,7 +375,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should have recorded the rule_id within the signal', async () => {
const rule: CreateRulesSchema = {
const rule: QueryCreateSchema = {
...getSimpleRule(),
index: ['signal_object_clash'],
from: '1900-01-01T00:00:00.000Z',
@ -388,7 +388,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should query and get back expected signal structure using a basic KQL query', async () => {
const rule: CreateRulesSchema = {
const rule: QueryCreateSchema = {
...getSimpleRule(),
index: ['signal_object_clash'],
from: '1900-01-01T00:00:00.000Z',
@ -437,7 +437,7 @@ export default ({ getService }: FtrProviderContext) => {
it('should query and get back expected signal structure when it is a signal on a signal', async () => {
// create a 1 signal from 1 auditbeat record
const rule: CreateRulesSchema = {
const rule: QueryCreateSchema = {
...getSimpleRule(),
index: ['signal_object_clash'],
from: '1900-01-01T00:00:00.000Z',
@ -447,7 +447,7 @@ export default ({ getService }: FtrProviderContext) => {
await waitForSignalsToBePresent(supertest, 1);
// Run signals on top of that 1 signal which should create a single signal (on top of) a signal
const ruleForSignals: CreateRulesSchema = {
const ruleForSignals: QueryCreateSchema = {
...getSimpleRule(),
rule_id: 'signal-on-signal',
index: [`${DEFAULT_SIGNALS_INDEX}*`],

View file

@ -21,6 +21,7 @@ import {
getSimpleRuleUpdate,
getSimpleMlRuleUpdate,
createRule,
getSimpleRule,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
@ -40,7 +41,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update a single rule property of name using a rule_id', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
// update a simple rule's name
const updatedRule = getSimpleRuleUpdate('rule-1');
@ -84,7 +85,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update a single rule property of name using an auto-generated rule_id', async () => {
const rule = getSimpleRuleUpdate('rule-1');
const rule = getSimpleRule('rule-1');
delete rule.rule_id;
const createRuleBody = await createRule(supertest, rule);
@ -108,7 +109,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update a single rule property of name using the auto-generated id', async () => {
const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1'));
const createdBody = await createRule(supertest, getSimpleRule('rule-1'));
// update a simple rule's name
const updatedRule = getSimpleRuleUpdate('rule-1');
@ -130,7 +131,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should change the version of a rule when it updates enabled and another property', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
// update a simple rule's enabled to false and another property
const updatedRule = getSimpleRuleUpdate('rule-1');
@ -153,7 +154,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
const ruleUpdate = getSimpleRuleUpdate('rule-1');
ruleUpdate.timeline_title = 'some title';

View file

@ -18,6 +18,7 @@ import {
removeServerGeneratedPropertiesIncludingRuleId,
getSimpleRuleUpdate,
createRule,
getSimpleRule,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
@ -37,7 +38,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update a single rule property of name using a rule_id', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
const updatedRule = getSimpleRuleUpdate('rule-1');
updatedRule.name = 'some other name';
@ -57,7 +58,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update two rule properties of name using the two rules rule_id', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
// create a second simple rule
await supertest
@ -94,7 +95,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update a single rule property of name using an id', async () => {
const createRuleBody = await createRule(supertest, getSimpleRuleUpdate('rule-1'));
const createRuleBody = await createRule(supertest, getSimpleRule('rule-1'));
// update a simple rule's name
const updatedRule1 = getSimpleRuleUpdate('rule-1');
@ -116,8 +117,8 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update two rule properties of name using the two rules id', async () => {
const createRule1 = await createRule(supertest, getSimpleRuleUpdate('rule-1'));
const createRule2 = await createRule(supertest, getSimpleRuleUpdate('rule-2'));
const createRule1 = await createRule(supertest, getSimpleRule('rule-1'));
const createRule2 = await createRule(supertest, getSimpleRule('rule-2'));
// update both rule names
const updatedRule1 = getSimpleRuleUpdate('rule-1');
@ -151,7 +152,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update a single rule property of name using the auto-generated id', async () => {
const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1'));
const createdBody = await createRule(supertest, getSimpleRule('rule-1'));
// update a simple rule's name
const updatedRule1 = getSimpleRuleUpdate('rule-1');
@ -173,7 +174,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should change the version of a rule when it updates enabled and another property', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
// update a simple rule's enabled to false and another property
const updatedRule1 = getSimpleRuleUpdate('rule-1');
@ -196,7 +197,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
// update a simple rule's timeline_title
const ruleUpdate = getSimpleRuleUpdate('rule-1');
@ -269,7 +270,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update one rule property and give an error about a second fake rule_id', async () => {
await createRule(supertest, getSimpleRuleUpdate('rule-1'));
await createRule(supertest, getSimpleRule('rule-1'));
const ruleUpdate = getSimpleRuleUpdate('rule-1');
ruleUpdate.name = 'some other name';
@ -304,7 +305,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should update one rule property and give an error about a second fake id', async () => {
const createdBody = await createRule(supertest, getSimpleRuleUpdate('rule-1'));
const createdBody = await createRule(supertest, getSimpleRule('rule-1'));
// update one rule name and give a fake id for the second
const rule1 = getSimpleRuleUpdate();

View file

@ -9,6 +9,12 @@ import { SuperTest } from 'supertest';
import supertestAsPromised from 'supertest-as-promised';
import { Context } from '@elastic/elasticsearch/lib/Transport';
import { SearchResponse } from 'elasticsearch';
import {
CreateRulesSchema,
UpdateRulesSchema,
FullResponseSchema,
QueryCreateSchema,
} from '../../plugins/security_solution/common/detection_engine/schemas/request';
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../../plugins/lists/common/constants';
import {
CreateExceptionListItemSchema,
@ -21,8 +27,6 @@ import {
Status,
SignalIds,
} from '../../plugins/security_solution/common/detection_engine/schemas/common/schemas';
import { CreateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema';
import { UpdateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema';
import { RulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema';
import {
DETECTION_ENGINE_INDEX_URL,
@ -37,8 +41,8 @@ import {
* @param rule Rule to pass in to remove typical server generated properties
*/
export const removeServerGeneratedProperties = (
rule: Partial<RulesSchema>
): Partial<RulesSchema> => {
rule: FullResponseSchema
): Partial<FullResponseSchema> => {
const {
/* eslint-disable @typescript-eslint/naming-convention */
created_at,
@ -61,8 +65,8 @@ export const removeServerGeneratedProperties = (
* @param rule Rule to pass in to remove typical server generated properties
*/
export const removeServerGeneratedPropertiesIncludingRuleId = (
rule: Partial<RulesSchema>
): Partial<RulesSchema> => {
rule: FullResponseSchema
): Partial<FullResponseSchema> => {
const ruleWithRemovedProperties = removeServerGeneratedProperties(rule);
// eslint-disable-next-line @typescript-eslint/naming-convention
const { rule_id, ...additionalRuledIdRemoved } = ruleWithRemovedProperties;
@ -74,7 +78,7 @@ export const removeServerGeneratedPropertiesIncludingRuleId = (
* @param ruleId
* @param enabled Enables the rule on creation or not. Defaulted to false to enable it on import
*/
export const getSimpleRule = (ruleId = 'rule-1', enabled = true): CreateRulesSchema => ({
export const getSimpleRule = (ruleId = 'rule-1', enabled = true): QueryCreateSchema => ({
name: 'Simple Rule Query',
description: 'Simple Rule Query',
enabled,
@ -384,7 +388,7 @@ export const getSimpleRuleAsNdjson = (ruleIds: string[], enabled = false): Buffe
* testing upload features.
* @param rule The rule to convert to ndjson
*/
export const ruleToNdjson = (rule: Partial<CreateRulesSchema>): Buffer => {
export const ruleToNdjson = (rule: CreateRulesSchema): Buffer => {
const stringified = JSON.stringify(rule);
return Buffer.from(`${stringified}\n`);
};
@ -725,7 +729,7 @@ export const countDownTest = async (
export const createRule = async (
supertest: SuperTest<supertestAsPromised.Test>,
rule: CreateRulesSchema
): Promise<RulesSchema> => {
): Promise<FullResponseSchema> => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')