mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* 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:
parent
c19d74c508
commit
79f9df06cf
61 changed files with 1921 additions and 4261 deletions
|
@ -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>;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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(),
|
||||
|
|
|
@ -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),
|
||||
];
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
});
|
File diff suppressed because it is too large
Load diff
|
@ -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>;
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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',
|
||||
});
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
};
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)];
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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({});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]]);
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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[] = [];
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
|
@ -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>;
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}*`],
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue