Skip to content

Commit

Permalink
[Security Solution][Detections] Adds framework for replacing API sche…
Browse files Browse the repository at this point in the history
…mas (#82462)

* 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
  • Loading branch information
marshallmain committed Nov 13, 2020
1 parent c19d74c commit f2ec573
Show file tree
Hide file tree
Showing 61 changed files with 1,921 additions and 4,261 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -66,14 +71,30 @@ 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
*/
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,
Expand All @@ -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',
Expand Down Expand Up @@ -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>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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({});
});

Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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',
};
Expand All @@ -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',
};
Expand All @@ -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',
};
Expand All @@ -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];
Expand All @@ -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', () => {
Expand All @@ -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(),
Expand All @@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Loading

0 comments on commit f2ec573

Please sign in to comment.