[SIEM][Detection Engine] Converts from joi to use io-ts and moves the types to common (#68127)

## Summary
* https://github.com/elastic/siem-team/issues/646
* Converts the detection rules and REST to use io-ts
* Removes their joi counterparts
* Updates all tests to use it
* Fixes a bug with the risk_score that was being sent in as a string from the UI instead of a number
* Fixes a bug within the exactCheck validating where it can now accept null value types for optional body messages.
* Fixes a bug in the FindRoute where it did not send down fields from REST
* Changes the lists plugin to utilize the io-ts types from siem rather than having them duplicated.
* Makes some stronger validations
* Adds a lot of codecs

**Things to look out for:**

* Generic testing to ensure I didn't break something that was not part of the tests.
* Fix for the risk_score from string to number is in:
```
x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx
```
* Fix for the exact check (unit tests are written and added)
```
x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx
```
* Within all the types I added are there any misspelled things or copy-pasta mistakes with strings:
x-pack/plugins/security_solution/common/detection_engine/schemas/types
* Fix for `find_rules_route.ts:58`
```
x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts
```

**Follow on things that this PR doesn't do we need to:**
* Add linter rule to forbid NodeJS code within common section
* The `[object Object]` formatter issues seen in the code such as:
```
// TODO: Fix/Change the formatErrors to be better able to handle objects
'Invalid value "[object Object]" supplied to "note"',
```
* Formatter issues such as: `'Invalid value "" supplied to ""'`
* Remove the hapi server object from lists plugin

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios
This commit is contained in:
Frank Hassanabad 2020-06-08 19:54:09 -06:00 committed by GitHub
parent e49888f2ec
commit d99cf75814
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
230 changed files with 13701 additions and 10845 deletions

View file

@ -8,8 +8,8 @@
import * as t from 'io-ts';
import { DefaultStringArray, NonEmptyString } from '../types';
import { DefaultNamespace } from '../types/default_namespace';
import { DefaultStringArray, NonEmptyString } from '../../siem_common_deps';
export const name = t.string;
export type Name = t.TypeOf<typeof name>;

View file

@ -24,8 +24,9 @@ import {
tags,
} from '../common/schemas';
import { Identity, RequiredKeepUndefined } from '../../types';
import { DefaultEntryArray, DefaultUuid } from '../types';
import { DefaultEntryArray } from '../types';
import { EntriesArray } from '../types/entries';
import { DefaultUuid } from '../../siem_common_deps';
export const createExceptionListItemSchema = t.intersection([
t.exact(

View file

@ -22,7 +22,7 @@ import {
tags,
} from '../common/schemas';
import { Identity, RequiredKeepUndefined } from '../../types';
import { DefaultUuid } from '../types/default_uuid';
import { DefaultUuid } from '../../siem_common_deps';
export const createExceptionListSchema = t.intersection([
t.exact(

View file

@ -6,6 +6,7 @@
/* eslint-disable @typescript-eslint/camelcase */
// TODO: You cannot import a stream from common into the front end code! CHANGE THIS
import { Readable } from 'stream';
import * as t from 'io-ts';
@ -20,6 +21,7 @@ export const importListItemSchema = t.exact(
export type ImportListItemSchema = t.TypeOf<typeof importListItemSchema>;
// TODO: You cannot import a stream from common into the front end code! CHANGE THIS
export interface HapiReadableStream extends Readable {
hapi: {
filename: string;

View file

@ -4,7 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export * from './default_entries_array';
export * from './default_string_array';
export * from './default_uuid';
export * from './entries';
export * from './non_empty_string';

View file

@ -4,5 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { NonEmptyString } from '../../security_solution/common/detection_engine/schemas/types/non_empty_string';
export { DefaultUuid } from '../../security_solution/common/detection_engine/schemas/types/default_uuid';
export { DefaultStringArray } from '../../security_solution/common/detection_engine/schemas/types/default_string_array';
export { exactCheck } from '../../security_solution/common/exact_check';
export { getPaths, foldLeftRight } from '../../security_solution/common/test_utils';

View file

@ -14,16 +14,35 @@ import { PositiveIntegerGreaterThanZero } from '../types/positive_integer_greate
import { PositiveInteger } from '../types/positive_integer';
export const description = t.string;
export type Description = t.TypeOf<typeof description>;
export const descriptionOrUndefined = t.union([description, t.undefined]);
export type DescriptionOrUndefined = t.TypeOf<typeof descriptionOrUndefined>;
export const enabled = t.boolean;
export const exclude_export_details = t.boolean;
export type Enabled = t.TypeOf<typeof enabled>;
export const enabledOrUndefined = t.union([enabled, t.undefined]);
export type EnabledOrUndefined = t.TypeOf<typeof enabledOrUndefined>;
export const false_positives = t.array(t.string);
export type FalsePositives = t.TypeOf<typeof false_positives>;
export const falsePositivesOrUndefined = t.union([false_positives, t.undefined]);
export type FalsePositivesOrUndefined = t.TypeOf<typeof falsePositivesOrUndefined>;
export const file_name = t.string;
export type FileName = t.TypeOf<typeof file_name>;
export const exclude_export_details = t.boolean;
export type ExcludeExportDetails = t.TypeOf<typeof exclude_export_details>;
/**
* TODO: Right now the filters is an "unknown", when it could more than likely
* become the actual ESFilter as a type.
*/
export const filters = t.array(t.unknown); // Filters are not easily type-able yet
export type Filters = t.TypeOf<typeof filters>; // Filters are not easily type-able yet
/**
* Params is an "object", since it is a type of AlertActionParams which is action templates.
@ -43,30 +62,98 @@ export const action = t.exact(
);
export const actions = t.array(action);
export type Actions = t.TypeOf<typeof actions>;
// TODO: Create a regular expression type or custom date math part type here
export const from = t.string;
export type From = t.TypeOf<typeof from>;
export const fromOrUndefined = t.union([from, t.undefined]);
export type FromOrUndefined = t.TypeOf<typeof fromOrUndefined>;
export const immutable = t.boolean;
export type Immutable = t.TypeOf<typeof immutable>;
// Note: Never make this a strict uuid, we allow the rule_id to be any string at the moment
// in case we encounter 3rd party rule systems which might be using auto incrementing numbers
// or other different things.
export const rule_id = t.string;
export type RuleId = t.TypeOf<typeof rule_id>;
export const ruleIdOrUndefined = t.union([rule_id, t.undefined]);
export type RuleIdOrUndefined = t.TypeOf<typeof ruleIdOrUndefined>;
export const id = UUID;
export const idOrUndefined = t.union([id, t.undefined]);
export type IdOrUndefined = t.TypeOf<typeof idOrUndefined>;
export const index = t.array(t.string);
export type Index = t.TypeOf<typeof index>;
export const indexOrUndefined = t.union([index, t.undefined]);
export type IndexOrUndefined = t.TypeOf<typeof indexOrUndefined>;
export const interval = t.string;
export type Interval = t.TypeOf<typeof interval>;
export const intervalOrUndefined = t.union([interval, t.undefined]);
export type IntervalOrUndefined = t.TypeOf<typeof intervalOrUndefined>;
export const query = t.string;
export type Query = t.TypeOf<typeof query>;
export const queryOrUndefined = t.union([query, t.undefined]);
export type QueryOrUndefined = t.TypeOf<typeof queryOrUndefined>;
export const language = t.keyof({ kuery: null, lucene: null });
export type Language = t.TypeOf<typeof language>;
export const languageOrUndefined = t.union([language, t.undefined]);
export type LanguageOrUndefined = t.TypeOf<typeof languageOrUndefined>;
export const objects = t.array(t.type({ rule_id }));
export const output_index = t.string;
export type OutputIndex = t.TypeOf<typeof output_index>;
export const outputIndexOrUndefined = t.union([output_index, t.undefined]);
export type OutputIndexOrUndefined = t.TypeOf<typeof outputIndexOrUndefined>;
export const saved_id = t.string;
export type SavedId = t.TypeOf<typeof saved_id>;
export const savedIdOrUndefined = t.union([saved_id, t.undefined]);
export type SavedIdOrUndefined = t.TypeOf<typeof savedIdOrUndefined>;
export const timeline_id = t.string;
export type TimelineId = t.TypeOf<typeof timeline_id>;
export const timelineIdOrUndefined = t.union([timeline_id, t.undefined]);
export type TimelineIdOrUndefined = t.TypeOf<typeof timelineIdOrUndefined>;
export const timeline_title = t.string;
export type TimelineTitle = t.TypeOf<typeof t.string>;
export const timelineTitleOrUndefined = t.union([timeline_title, t.undefined]);
export type TimelineTitleOrUndefined = t.TypeOf<typeof timelineTitleOrUndefined>;
export const throttle = t.string;
export type Throttle = t.TypeOf<typeof throttle>;
export const throttleOrNull = t.union([throttle, t.null]);
export type ThrottleOrNull = t.TypeOf<typeof throttleOrNull>;
export const anomaly_threshold = PositiveInteger;
export type AnomalyThreshold = t.TypeOf<typeof PositiveInteger>;
export const anomalyThresholdOrUndefined = t.union([anomaly_threshold, t.undefined]);
export type AnomalyThresholdOrUndefined = t.TypeOf<typeof anomalyThresholdOrUndefined>;
export const machine_learning_job_id = t.string;
export type MachineLearningJobId = t.TypeOf<typeof machine_learning_job_id>;
export const machineLearningJobIdOrUndefined = t.union([machine_learning_job_id, t.undefined]);
export type MachineLearningJobIdOrUndefined = t.TypeOf<typeof machineLearningJobIdOrUndefined>;
/**
* Note that this is a plain unknown object because we allow the UI
@ -76,30 +163,103 @@ export const machine_learning_job_id = t.string;
* so we have tighter control over 3rd party data structures.
*/
export const meta = t.object;
export type Meta = t.TypeOf<typeof meta>;
export const metaOrUndefined = t.union([meta, t.undefined]);
export type MetaOrUndefined = t.TypeOf<typeof metaOrUndefined>;
export const max_signals = PositiveIntegerGreaterThanZero;
export type MaxSignals = t.TypeOf<typeof max_signals>;
export const maxSignalsOrUndefined = t.union([max_signals, t.undefined]);
export type MaxSignalsOrUndefined = t.TypeOf<typeof maxSignalsOrUndefined>;
export const name = t.string;
export type Name = t.TypeOf<typeof name>;
export const nameOrUndefined = t.union([name, t.undefined]);
export type NameOrUndefined = t.TypeOf<typeof nameOrUndefined>;
export const risk_score = RiskScore;
export type RiskScore = t.TypeOf<typeof risk_score>;
export const riskScoreOrUndefined = t.union([risk_score, t.undefined]);
export type RiskScoreOrUndefined = t.TypeOf<typeof riskScoreOrUndefined>;
export const severity = t.keyof({ low: null, medium: null, high: null, critical: null });
export type Severity = t.TypeOf<typeof severity>;
export const severityOrUndefined = t.union([severity, t.undefined]);
export type SeverityOrUndefined = t.TypeOf<typeof severityOrUndefined>;
export const status = t.keyof({ open: null, closed: null });
export const job_status = t.keyof({ succeeded: null, failed: null, 'going to run': null });
// TODO: Create a regular expression type or custom date math part type here
export const to = t.string;
export type To = t.TypeOf<typeof to>;
export const toOrUndefined = t.union([to, t.undefined]);
export type ToOrUndefined = t.TypeOf<typeof toOrUndefined>;
export const type = t.keyof({ machine_learning: null, query: null, saved_query: null });
export type Type = t.TypeOf<typeof type>;
export const typeOrUndefined = t.union([type, t.undefined]);
export type TypeOrUndefined = t.TypeOf<typeof typeOrUndefined>;
export const queryFilter = t.string;
export type QueryFilter = t.TypeOf<typeof queryFilter>;
export const queryFilterOrUndefined = t.union([queryFilter, t.undefined]);
export type QueryFilterOrUndefined = t.TypeOf<typeof queryFilterOrUndefined>;
export const references = t.array(t.string);
export type References = t.TypeOf<typeof references>;
export const referencesOrUndefined = t.union([references, t.undefined]);
export type ReferencesOrUndefined = t.TypeOf<typeof referencesOrUndefined>;
export const per_page = PositiveInteger;
export type PerPage = t.TypeOf<typeof per_page>;
export const perPageOrUndefined = t.union([per_page, t.undefined]);
export type PerPageOrUndefined = t.TypeOf<typeof perPageOrUndefined>;
export const page = PositiveIntegerGreaterThanZero;
export type Page = t.TypeOf<typeof page>;
export const pageOrUndefined = t.union([page, t.undefined]);
export type PageOrUndefined = t.TypeOf<typeof pageOrUndefined>;
export const signal_ids = t.array(t.string);
// TODO: Can this be more strict or is this is the set of all Elastic Queries?
export const signal_status_query = t.object;
export const sort_field = t.string;
export type SortField = t.TypeOf<typeof sort_field>;
export const sortFieldOrUndefined = t.union([sort_field, t.undefined]);
export type SortFieldOrUndefined = t.TypeOf<typeof sortFieldOrUndefined>;
export const sort_order = t.keyof({ asc: null, desc: null });
export type sortOrder = t.TypeOf<typeof sort_order>;
export const sortOrderOrUndefined = t.union([sort_order, t.undefined]);
export type SortOrderOrUndefined = t.TypeOf<typeof sortOrderOrUndefined>;
export const tags = t.array(t.string);
export type Tags = t.TypeOf<typeof tags>;
export const tagsOrUndefined = t.union([tags, t.undefined]);
export type TagsOrUndefined = t.TypeOf<typeof tagsOrUndefined>;
export const fields = t.array(t.string);
export type Fields = t.TypeOf<typeof fields>;
export const fieldsOrUndefined = t.union([fields, t.undefined]);
export type FieldsOrUndefined = t.TypeOf<typeof fieldsOrUndefined>;
export const threat_framework = t.string;
export const threat_tactic_id = t.string;
export const threat_tactic_name = t.string;
@ -129,11 +289,23 @@ export const threat = t.array(
})
)
);
export type Threat = t.TypeOf<typeof threat>;
export const threatOrUndefined = t.union([threat, t.undefined]);
export type ThreatOrUndefined = t.TypeOf<typeof threatOrUndefined>;
export const created_at = IsoDateString;
export const updated_at = IsoDateString;
export const updated_by = t.string;
export const created_by = t.string;
export const version = PositiveIntegerGreaterThanZero;
export type Version = t.TypeOf<typeof version>;
export const versionOrUndefined = t.union([version, t.undefined]);
export type VersionOrUndefined = t.TypeOf<typeof versionOrUndefined>;
export const last_success_at = IsoDateString;
export const last_success_message = t.string;
export const last_failure_at = IsoDateString;
@ -150,7 +322,12 @@ export const success_count = PositiveInteger;
export const rules_custom_installed = PositiveInteger;
export const rules_not_installed = PositiveInteger;
export const rules_not_updated = PositiveInteger;
export const note = t.string;
export type Note = t.TypeOf<typeof note>;
export const noteOrUndefined = t.union([note, t.undefined]);
export type NoteOrUndefined = t.TypeOf<typeof noteOrUndefined>;
// NOTE: Experimental list support not being shipped currently and behind a feature flag
// TODO: Remove this comment once we lists have passed testing and is ready for the release
@ -185,3 +362,6 @@ export const list_and = t.intersection([
and: t.array(list),
}),
]);
export const listAndOrUndefined = t.union([t.array(list_and), t.undefined]);
export type ListAndOrUndefined = t.TypeOf<typeof listAndOrUndefined>;

View file

@ -0,0 +1,47 @@
/*
* 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 {
AddPrepackagedRulesSchema,
AddPrepackagedRulesSchemaDecoded,
} from './add_prepackaged_rules_schema';
import { DEFAULT_MAX_SIGNALS } from '../../../constants';
export const getAddPrepackagedRulesSchemaMock = (): AddPrepackagedRulesSchema => ({
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',
version: 1,
});
export const getAddPrepackagedRulesSchemaDecodedMock = (): AddPrepackagedRulesSchemaDecoded => ({
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',
references: [],
actions: [],
enabled: false,
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',
});

View file

@ -0,0 +1,134 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
/* eslint-disable @typescript-eslint/camelcase */
import {
description,
anomaly_threshold,
filters,
index,
saved_id,
timeline_id,
timeline_title,
meta,
machine_learning_job_id,
risk_score,
MaxSignals,
name,
severity,
Tags,
To,
type,
Threat,
ThrottleOrNull,
note,
References,
Actions,
Enabled,
FalsePositives,
From,
Interval,
language,
query,
rule_id,
version,
} from '../common/schemas';
/* eslint-enable @typescript-eslint/camelcase */
import { DefaultStringArray } from '../types/default_string_array';
import { DefaultActionsArray } from '../types/default_actions_array';
import { DefaultBooleanFalse } from '../types/default_boolean_false';
import { DefaultFromString } from '../types/default_from_string';
import { DefaultIntervalString } from '../types/default_interval_string';
import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number';
import { DefaultToString } from '../types/default_to_string';
import { DefaultThreatArray } from '../types/default_threat_array';
import { DefaultThrottleNull } from '../types/default_throttle_null';
import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array';
/**
* Big differences between this schema and the createRulesSchema
* - rule_id is required here
* - output_index is not allowed (and instead the space index must be used)
* - immutable is forbidden but defaults to true instead of to false and it can only ever be true (This is forced directly in the route and not here)
* - enabled defaults to false instead of true
* - version is a required field that must exist
* - index is a required field that must exist if type !== machine_learning (Checked within the runtime type dependent system)
*/
export const addPrepackagedRulesSchema = t.intersection([
t.exact(
t.type({
description,
risk_score,
name,
severity,
type,
rule_id,
version,
})
),
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
enabled: DefaultBooleanFalse, // defaults to false 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
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
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
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
throttle: DefaultThrottleNull, // defaults to "null" 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
exceptions_list: ListsDefaultArray, // defaults to empty array if not set during decode
})
),
]);
export type AddPrepackagedRulesSchema = t.TypeOf<typeof addPrepackagedRulesSchema>;
// This type is used after a decode since some things are defaults after a decode.
export type AddPrepackagedRulesSchemaDecoded = Omit<
AddPrepackagedRulesSchema,
| 'references'
| 'actions'
| 'enabled'
| 'false_positives'
| 'from'
| 'interval'
| 'max_signals'
| 'tags'
| 'to'
| 'threat'
| 'throttle'
| 'exceptions_list'
> & {
references: References;
actions: Actions;
enabled: Enabled;
false_positives: FalsePositives;
from: From;
interval: Interval;
max_signals: MaxSignals;
tags: Tags;
to: To;
threat: Threat;
throttle: ThrottleOrNull;
exceptions_list: ListsDefaultArraySchema;
};

View file

@ -0,0 +1,71 @@
/*
* 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 { AddPrepackagedRulesSchema } from './add_prepackaged_rules_schema';
import { addPrepackagedRuleValidateTypeDependents } from './add_prepackaged_rules_type_dependents';
import { getAddPrepackagedRulesSchemaMock } from './add_prepackaged_rules_schema.mock';
describe('create_rules_type_dependents', () => {
test('saved_id is required when type is saved_query and will not validate without out', () => {
const schema: AddPrepackagedRulesSchema = {
...getAddPrepackagedRulesSchemaMock(),
type: 'saved_query',
};
delete schema.saved_id;
const errors = addPrepackagedRuleValidateTypeDependents(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: AddPrepackagedRulesSchema = {
...getAddPrepackagedRulesSchemaMock(),
type: 'saved_query',
saved_id: '123',
};
const errors = addPrepackagedRuleValidateTypeDependents(schema);
expect(errors).toEqual([]);
});
test('You cannot omit timeline_title when timeline_id is present', () => {
const schema: AddPrepackagedRulesSchema = {
...getAddPrepackagedRulesSchemaMock(),
timeline_id: '123',
};
delete schema.timeline_title;
const errors = addPrepackagedRuleValidateTypeDependents(schema);
expect(errors).toEqual(['when "timeline_id" exists, "timeline_title" must also exist']);
});
test('You cannot have empty string for timeline_title when timeline_id is present', () => {
const schema: AddPrepackagedRulesSchema = {
...getAddPrepackagedRulesSchemaMock(),
timeline_id: '123',
timeline_title: '',
};
const errors = addPrepackagedRuleValidateTypeDependents(schema);
expect(errors).toEqual(['"timeline_title" cannot be an empty string']);
});
test('You cannot have timeline_title with an empty timeline_id', () => {
const schema: AddPrepackagedRulesSchema = {
...getAddPrepackagedRulesSchemaMock(),
timeline_id: '',
timeline_title: 'some-title',
};
const errors = addPrepackagedRuleValidateTypeDependents(schema);
expect(errors).toEqual(['"timeline_id" cannot be an empty string']);
});
test('You cannot have timeline_title without timeline_id', () => {
const schema: AddPrepackagedRulesSchema = {
...getAddPrepackagedRulesSchemaMock(),
timeline_title: 'some-title',
};
delete schema.timeline_id;
const errors = addPrepackagedRuleValidateTypeDependents(schema);
expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']);
});
});

View file

@ -0,0 +1,107 @@
/*
* 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 { AddPrepackagedRulesSchema } from './add_prepackaged_rules_schema';
export const validateAnomalyThreshold = (rule: AddPrepackagedRulesSchema): string[] => {
if (rule.type === 'machine_learning') {
if (rule.anomaly_threshold == null) {
return ['when "type" is "machine_learning" anomaly_threshold is required'];
} else {
return [];
}
} else {
return [];
}
};
export const validateQuery = (rule: AddPrepackagedRulesSchema): string[] => {
if (rule.type === 'machine_learning') {
if (rule.query != null) {
return ['when "type" is "machine_learning", "query" cannot be set'];
} else {
return [];
}
} else {
return [];
}
};
export const validateLanguage = (rule: AddPrepackagedRulesSchema): string[] => {
if (rule.type === 'machine_learning') {
if (rule.language != null) {
return ['when "type" is "machine_learning", "language" cannot be set'];
} else {
return [];
}
} else {
return [];
}
};
export const validateSavedId = (rule: AddPrepackagedRulesSchema): 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: AddPrepackagedRulesSchema): string[] => {
if (rule.type === 'machine_learning') {
if (rule.machine_learning_job_id == null) {
return ['when "type" is "machine_learning", "machine_learning_job_id" is required'];
} else {
return [];
}
} else {
return [];
}
};
export const validateTimelineId = (rule: AddPrepackagedRulesSchema): string[] => {
if (rule.timeline_id != null) {
if (rule.timeline_title == null) {
return ['when "timeline_id" exists, "timeline_title" must also exist'];
} else if (rule.timeline_id === '') {
return ['"timeline_id" cannot be an empty string'];
} else {
return [];
}
}
return [];
};
export const validateTimelineTitle = (rule: AddPrepackagedRulesSchema): string[] => {
if (rule.timeline_title != null) {
if (rule.timeline_id == null) {
return ['when "timeline_title" exists, "timeline_id" must also exist'];
} else if (rule.timeline_title === '') {
return ['"timeline_title" cannot be an empty string'];
} else {
return [];
}
}
return [];
};
export const addPrepackagedRuleValidateTypeDependents = (
schema: AddPrepackagedRulesSchema
): string[] => {
return [
...validateAnomalyThreshold(schema),
...validateQuery(schema),
...validateLanguage(schema),
...validateSavedId(schema),
...validateMachineLearningJobId(schema),
...validateTimelineId(schema),
...validateTimelineTitle(schema),
];
};

View file

@ -0,0 +1,281 @@
/*
* 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 {
createRulesBulkSchema,
CreateRulesBulkSchema,
CreateRulesBulkSchemaDecoded,
} 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';
// only the basics of testing are here.
// see: create_rules_schema.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', () => {
const payload: CreateRulesBulkSchema = [];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(output.errors).toEqual([]);
expect(output.schema).toEqual([]);
});
test('made up values do not validate for a single element', () => {
const payload: Array<{ madeUp: string }> = [{ madeUp: 'hi' }];
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(output.schema).toEqual({});
});
test('single array element does validate', () => {
const payload: CreateRulesBulkSchema = [getCreateRulesSchemaMock()];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([getCreateRulesSchemaDecodedMock()]);
});
test('two array elements do validate', () => {
const payload: CreateRulesBulkSchema = [getCreateRulesSchemaMock(), getCreateRulesSchemaMock()];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([
getCreateRulesSchemaDecodedMock(),
getCreateRulesSchemaDecodedMock(),
]);
});
test('single array element with a missing value (risk_score) will not validate', () => {
const singleItem = getCreateRulesSchemaMock();
delete singleItem.risk_score;
const payload: CreateRulesBulkSchema = [singleItem];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
});
test('two array elements where the first is valid but the second is invalid (risk_score) will not validate', () => {
const singleItem = getCreateRulesSchemaMock();
const secondItem = getCreateRulesSchemaMock();
delete secondItem.risk_score;
const payload: CreateRulesBulkSchema = [singleItem, secondItem];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
});
test('two array elements where the first is invalid (risk_score) but the second is valid will not validate', () => {
const singleItem = getCreateRulesSchemaMock();
const secondItem = getCreateRulesSchemaMock();
delete singleItem.risk_score;
const payload: CreateRulesBulkSchema = [singleItem, secondItem];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
});
test('two array elements where both are invalid (risk_score) will not validate', () => {
const singleItem = getCreateRulesSchemaMock();
const secondItem = getCreateRulesSchemaMock();
delete singleItem.risk_score;
delete secondItem.risk_score;
const payload: CreateRulesBulkSchema = [singleItem, secondItem];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
});
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 } = {
...getCreateRulesSchemaMock(),
madeUpValue: 'something',
};
const secondItem = getCreateRulesSchemaMock();
const payload: CreateRulesBulkSchema = [singleItem, secondItem];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue"']);
expect(output.schema).toEqual({});
});
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 } = {
...getCreateRulesSchemaMock(),
madeUpValue: 'something',
};
const payload: CreateRulesBulkSchema = [singleItem, secondItem];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue"']);
expect(output.schema).toEqual({});
});
test('two array elements where both are invalid (extra key and value) will not validate', () => {
const singleItem: CreateRulesSchema & { madeUpValue: string } = {
...getCreateRulesSchemaMock(),
madeUpValue: 'something',
};
const secondItem: CreateRulesSchema & { madeUpValue: string } = {
...getCreateRulesSchemaMock(),
madeUpValue: 'something',
};
const payload: CreateRulesBulkSchema = [singleItem, secondItem];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue,madeUpValue"']);
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];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['Invalid value "madeup" supplied to "severity"']);
expect(output.schema).toEqual({});
});
test('You can set "note" to a string', () => {
const payload: CreateRulesBulkSchema = [
{ ...getCreateRulesSchemaMock(), note: '# test markdown' },
];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([
{ ...getCreateRulesSchemaDecodedMock(), note: '# test markdown' },
]);
});
test('You can set "note" to an empty string', () => {
const payload: CreateRulesBulkSchema = [{ ...getCreateRulesSchemaMock(), note: '' }];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([{ ...getCreateRulesSchemaDecodedMock(), note: '' }]);
});
test('You can set "note" to anything other than string', () => {
const payload = [
{
...getCreateRulesSchemaMock(),
note: {
something: 'some object',
},
},
];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
// TODO: We should change the formatter used to better print objects
expect(formatErrors(output.errors)).toEqual([
'Invalid value "[object Object]" supplied to "note"',
]);
expect(output.schema).toEqual({});
});
test('The default for "actions" will be an empty array', () => {
const { actions, ...withoutActions } = getCreateRulesSchemaMock();
const payload: CreateRulesBulkSchema = [withoutActions];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect((output.schema as CreateRulesBulkSchemaDecoded)[0].actions).toEqual([]);
});
test('The default for "throttle" will be null', () => {
const { throttle, ...withoutThrottle } = getCreateRulesSchemaMock();
const payload: CreateRulesBulkSchema = [withoutThrottle];
const decoded = createRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect((output.schema as CreateRulesBulkSchemaDecoded)[0].throttle).toEqual(null);
});
});

View file

@ -0,0 +1,14 @@
/*
* 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 { createRulesSchema, CreateRulesSchemaDecoded } from './create_rules_schema';
export const createRulesBulkSchema = t.array(createRulesSchema);
export type CreateRulesBulkSchema = t.TypeOf<typeof createRulesBulkSchema>;
export type CreateRulesBulkSchemaDecoded = CreateRulesSchemaDecoded[];

View file

@ -0,0 +1,43 @@
/*
* 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 = (): CreateRulesSchema => ({
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 getCreateRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => ({
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',
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',
});

View file

@ -0,0 +1,134 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
/* eslint-disable @typescript-eslint/camelcase */
import {
description,
anomaly_threshold,
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,
ThrottleOrNull,
note,
Version,
References,
Actions,
Enabled,
FalsePositives,
From,
Interval,
language,
query,
} from '../common/schemas';
/* eslint-enable @typescript-eslint/camelcase */
import { DefaultStringArray } from '../types/default_string_array';
import { DefaultActionsArray } from '../types/default_actions_array';
import { DefaultBooleanTrue } from '../types/default_boolean_true';
import { DefaultFromString } from '../types/default_from_string';
import { DefaultIntervalString } from '../types/default_interval_string';
import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number';
import { DefaultToString } from '../types/default_to_string';
import { DefaultThreatArray } from '../types/default_threat_array';
import { DefaultThrottleNull } from '../types/default_throttle_null';
import { DefaultVersionNumber } from '../types/default_version_number';
import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array';
import { DefaultUuid } from '../types/default_uuid';
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
enabled: DefaultBooleanTrue, // defaults to true 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
// 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
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
throttle: DefaultThrottleNull, // defaults to "null" 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: ListsDefaultArray, // defaults to empty array 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,
| 'references'
| 'actions'
| 'enabled'
| 'false_positives'
| 'from'
| 'interval'
| 'max_signals'
| 'tags'
| 'to'
| 'threat'
| 'throttle'
| 'version'
| 'exceptions_list'
| 'rule_id'
> & {
references: References;
actions: Actions;
enabled: Enabled;
false_positives: FalsePositives;
from: From;
interval: Interval;
max_signals: MaxSignals;
tags: Tags;
to: To;
threat: Threat;
throttle: ThrottleOrNull;
version: Version;
exceptions_list: ListsDefaultArraySchema;
rule_id: RuleId;
};

View file

@ -0,0 +1,68 @@
/*
* 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 { getCreateRulesSchemaMock } from './create_rules_schema.mock';
import { CreateRulesSchema } from './create_rules_schema';
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(),
timeline_id: '123',
};
delete schema.timeline_title;
const errors = createRuleValidateTypeDependents(schema);
expect(errors).toEqual(['when "timeline_id" exists, "timeline_title" must also exist']);
});
test('You cannot have empty string for timeline_title when timeline_id is present', () => {
const schema: CreateRulesSchema = {
...getCreateRulesSchemaMock(),
timeline_id: '123',
timeline_title: '',
};
const errors = createRuleValidateTypeDependents(schema);
expect(errors).toEqual(['"timeline_title" cannot be an empty string']);
});
test('You cannot have timeline_title with an empty timeline_id', () => {
const schema: CreateRulesSchema = {
...getCreateRulesSchemaMock(),
timeline_id: '',
timeline_title: 'some-title',
};
const errors = createRuleValidateTypeDependents(schema);
expect(errors).toEqual(['"timeline_id" cannot be an empty string']);
});
test('You cannot have timeline_title without timeline_id', () => {
const schema: CreateRulesSchema = {
...getCreateRulesSchemaMock(),
timeline_title: 'some-title',
};
delete schema.timeline_id;
const errors = createRuleValidateTypeDependents(schema);
expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']);
});
});

View file

@ -0,0 +1,105 @@
/*
* 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 } from './create_rules_schema';
export const validateAnomalyThreshold = (rule: CreateRulesSchema): string[] => {
if (rule.type === 'machine_learning') {
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 (rule.type === 'machine_learning') {
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 (rule.type === 'machine_learning') {
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 (rule.type === 'machine_learning') {
if (rule.machine_learning_job_id == null) {
return ['when "type" is "machine_learning", "machine_learning_job_id" is required'];
} else {
return [];
}
} else {
return [];
}
};
export const validateTimelineId = (rule: CreateRulesSchema): string[] => {
if (rule.timeline_id != null) {
if (rule.timeline_title == null) {
return ['when "timeline_id" exists, "timeline_title" must also exist'];
} else if (rule.timeline_id === '') {
return ['"timeline_id" cannot be an empty string'];
} else {
return [];
}
}
return [];
};
export const validateTimelineTitle = (rule: CreateRulesSchema): string[] => {
if (rule.timeline_title != null) {
if (rule.timeline_id == null) {
return ['when "timeline_title" exists, "timeline_id" must also exist'];
} else if (rule.timeline_title === '') {
return ['"timeline_title" cannot be an empty string'];
} else {
return [];
}
}
return [];
};
export const createRuleValidateTypeDependents = (schema: CreateRulesSchema): string[] => {
return [
...validateAnomalyThreshold(schema),
...validateQuery(schema),
...validateLanguage(schema),
...validateSavedId(schema),
...validateMachineLearningJobId(schema),
...validateTimelineId(schema),
...validateTimelineTitle(schema),
];
};

View file

@ -0,0 +1,159 @@
/*
* 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 {
exportRulesQuerySchema,
exportRulesSchema,
ExportRulesSchema,
ExportRulesQuerySchema,
ExportRulesQuerySchemaDecoded,
} from './export_rules_schema';
import { exactCheck } from '../../../exact_check';
import { pipe } from 'fp-ts/lib/pipeable';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { left } from 'fp-ts/lib/Either';
describe('create rules schema', () => {
describe('exportRulesSchema', () => {
test('null value or absent values validate', () => {
const payload: Partial<ExportRulesSchema> = null;
const decoded = exportRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('empty object does not validate', () => {
const payload = {};
const decoded = exportRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
// TODO: Change formatter to display a better value than [object Object]
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "objects"',
'Invalid value "[object Object]" supplied to ""',
]);
expect(message.schema).toEqual(payload);
});
test('empty object array does validate', () => {
const payload: ExportRulesSchema = { objects: [] };
const decoded = exportRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('array with rule_id validates', () => {
const payload: ExportRulesSchema = { objects: [{ rule_id: 'test-1' }] };
const decoded = exportRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('array with id does not validate as we do not allow that on purpose since we export rule_id', () => {
const payload: Omit<ExportRulesSchema, 'objects'> & { objects: [{ id: string }] } = {
objects: [{ id: '4a7ff83d-3055-4bb2-ba68-587b9c6c15a4' }],
};
const decoded = exportRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
// TODO: Change formatter to display a better value than [object Object]
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "objects,rule_id"',
'Invalid value "[object Object]" supplied to ""',
]);
expect(message.schema).toEqual({});
});
});
describe('exportRulesQuerySchema', () => {
test('default value for file_name is export.ndjson and default for exclude_export_details is false', () => {
const payload: Partial<ExportRulesQuerySchema> = {};
const decoded = exportRulesQuerySchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
const expected: ExportRulesQuerySchemaDecoded = {
file_name: 'export.ndjson',
exclude_export_details: false,
};
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(expected);
});
test('file_name validates', () => {
const payload: ExportRulesQuerySchema = {
file_name: 'test.ndjson',
};
const decoded = exportRulesQuerySchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
const expected: ExportRulesQuerySchemaDecoded = {
file_name: 'test.ndjson',
exclude_export_details: false,
};
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(expected);
});
test('file_name does not validate with a number', () => {
const payload: Omit<ExportRulesQuerySchema, 'file_name'> & { file_name: number } = {
file_name: 10,
};
const decoded = exportRulesQuerySchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "10" supplied to ""']);
expect(message.schema).toEqual({});
});
test('exclude_export_details validates with a boolean true', () => {
const payload: ExportRulesQuerySchema = {
exclude_export_details: true,
};
const decoded = exportRulesQuerySchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
const expected: ExportRulesQuerySchemaDecoded = {
exclude_export_details: true,
file_name: 'export.ndjson',
};
expect(message.schema).toEqual(expected);
});
test('exclude_export_details does not validate with a string', () => {
const payload: Omit<ExportRulesQuerySchema, 'exclude_export_details'> & {
exclude_export_details: string;
} = {
exclude_export_details: 'invalid string',
};
const decoded = exportRulesQuerySchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "invalid string" supplied to ""',
]);
expect(message.schema).toEqual({});
});
});
});

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
/* eslint-disable @typescript-eslint/camelcase */
import { rule_id, FileName, ExcludeExportDetails } from '../common/schemas';
/* eslint-enable @typescript-eslint/camelcase */
import { DefaultExportFileName } from '../types/default_export_file_name';
import { DefaultStringBooleanFalse } from '../types/default_string_boolean_false';
const objects = t.array(t.exact(t.type({ rule_id })));
export const exportRulesSchema = t.union([t.exact(t.type({ objects })), t.null]);
export type ExportRulesSchema = t.TypeOf<typeof exportRulesSchema>;
export type ExportRulesSchemaDecoded = ExportRulesSchema;
export const exportRulesQuerySchema = t.exact(
t.partial({ file_name: DefaultExportFileName, exclude_export_details: DefaultStringBooleanFalse })
);
export type ExportRulesQuerySchema = t.TypeOf<typeof exportRulesQuerySchema>;
export type ExportRulesQuerySchemaDecoded = Omit<
ExportRulesQuerySchema,
'file_name' | 'exclude_export_details'
> & {
file_name: FileName;
exclude_export_details: ExcludeExportDetails;
};

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
export const findRulesStatusesSchema = t.exact(
t.type({
ids: t.array(t.string),
})
);
export type FindRulesStatusesSchema = t.TypeOf<typeof findRulesStatusesSchema>;
export type FindRulesStatusesSchemaDecoded = FindRulesStatusesSchema;

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FindRulesSchema } from './find_rules_schema';
import { findRuleValidateTypeDependents } from './find_rules_type_dependents';
describe('find_rules_type_dependents', () => {
test('You can have an empty sort_field and empty sort_order', () => {
const schema: FindRulesSchema = {};
const errors = findRuleValidateTypeDependents(schema);
expect(errors).toEqual([]);
});
test('You can have both a sort_field and and a sort_order', () => {
const schema: FindRulesSchema = {
sort_field: 'some field',
sort_order: 'asc',
};
const errors = findRuleValidateTypeDependents(schema);
expect(errors).toEqual([]);
});
test('You cannot have sort_field without sort_order', () => {
const schema: FindRulesSchema = {
sort_field: 'some field',
};
const errors = findRuleValidateTypeDependents(schema);
expect(errors).toEqual([
'when "sort_order" and "sort_field" must exist together or not at all',
]);
});
test('You cannot have sort_order without sort_field', () => {
const schema: FindRulesSchema = {
sort_order: 'asc',
};
const errors = findRuleValidateTypeDependents(schema);
expect(errors).toEqual([
'when "sort_order" and "sort_field" must exist together or not at all',
]);
});
});

View file

@ -0,0 +1,198 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { exactCheck } from '../../../exact_check';
import { pipe } from 'fp-ts/lib/pipeable';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { left } from 'fp-ts/lib/Either';
import { FindRulesSchema, findRulesSchema } from './find_rules_schema';
describe('find_rules_schema', () => {
test('empty objects do validate', () => {
const payload: FindRulesSchema = {};
const decoded = findRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({
page: 1,
per_page: 20,
});
});
test('all values validate', () => {
const payload: FindRulesSchema = {
per_page: 5,
page: 1,
sort_field: 'some field',
fields: ['field 1', 'field 2'],
filter: 'some filter',
sort_order: 'asc',
};
const decoded = findRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('made up parameters do not validate', () => {
const payload: Partial<FindRulesSchema> & { madeUp: string } = { madeUp: 'invalid value' };
const decoded = findRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeUp"']);
expect(message.schema).toEqual({});
});
test('per_page validates', () => {
const payload: FindRulesSchema = {
per_page: 5,
};
const decoded = findRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesSchema).per_page).toEqual(payload.per_page);
});
test('page validates', () => {
const payload: FindRulesSchema = {
page: 5,
};
const decoded = findRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesSchema).page).toEqual(payload.page);
});
test('sort_field validates', () => {
const payload: FindRulesSchema = {
sort_field: 'value',
};
const decoded = findRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesSchema).sort_field).toEqual('value');
});
test('fields validates with a string', () => {
const payload: FindRulesSchema = {
fields: ['some value'],
};
const decoded = findRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesSchema).fields).toEqual(payload.fields);
});
test('fields validates with multiple strings', () => {
const payload: FindRulesSchema = {
fields: ['some value 1', 'some value 2'],
};
const decoded = findRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesSchema).fields).toEqual(payload.fields);
});
test('fields does not validate with a number', () => {
const payload: Omit<FindRulesSchema, 'fields'> & { fields: number } = {
fields: 5,
};
const decoded = findRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "fields"']);
expect(message.schema).toEqual({});
});
test('per_page has a default of 20', () => {
const payload: FindRulesSchema = {};
const decoded = findRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesSchema).per_page).toEqual(20);
});
test('page has a default of 1', () => {
const payload: FindRulesSchema = {};
const decoded = findRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesSchema).page).toEqual(1);
});
test('filter works with a string', () => {
const payload: FindRulesSchema = {
filter: 'some value 1',
};
const decoded = findRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesSchema).filter).toEqual(payload.filter);
});
test('filter does not work with a number', () => {
const payload: Omit<FindRulesSchema, 'filter'> & { filter: number } = {
filter: 5,
};
const decoded = findRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "filter"']);
expect(message.schema).toEqual({});
});
test('sort_order validates with desc and sort_field', () => {
const payload: FindRulesSchema = {
sort_order: 'desc',
sort_field: 'some field',
};
const decoded = findRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesSchema).sort_order).toEqual(payload.sort_order);
expect((message.schema as FindRulesSchema).sort_field).toEqual(payload.sort_field);
});
test('sort_order does not validate with a string other than asc and desc', () => {
const payload: Omit<FindRulesSchema, 'sort_order'> & { sort_order: string } = {
sort_order: 'some other string',
sort_field: 'some field',
};
const decoded = findRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "some other string" supplied to "sort_order"',
]);
expect(message.schema).toEqual({});
});
});

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
/* eslint-disable @typescript-eslint/camelcase */
import { queryFilter, fields, sort_field, sort_order, PerPage, Page } from '../common/schemas';
import { DefaultPerPage } from '../types/default_per_page';
import { DefaultPage } from '../types/default_page';
/* eslint-enable @typescript-eslint/camelcase */
export const findRulesSchema = t.exact(
t.partial({
fields,
filter: queryFilter,
per_page: DefaultPerPage, // defaults to "20" if not sent in during decode
page: DefaultPage, // defaults to "1" if not sent in during decode
sort_field,
sort_order,
})
);
export type FindRulesSchema = t.TypeOf<typeof findRulesSchema>;
export type FindRulesSchemaDecoded = Omit<FindRulesSchema, 'per_page'> & {
per_page: PerPage;
page: Page;
};

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FindRulesSchema } from './find_rules_schema';
export const validateSortOrder = (find: FindRulesSchema): string[] => {
if (find.sort_order != null || find.sort_field != null) {
if (find.sort_order == null || find.sort_field == null) {
return ['when "sort_order" and "sort_field" must exist together or not at all'];
} else {
return [];
}
} else {
return [];
}
};
export const findRuleValidateTypeDependents = (schema: FindRulesSchema): string[] => {
return [...validateSortOrder(schema)];
};

View file

@ -0,0 +1,44 @@
/*
* 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 { ImportRulesSchema, ImportRulesSchemaDecoded } from './import_rules_schema';
import { DEFAULT_MAX_SIGNALS } from '../../../constants';
export const getImportRulesSchemaMock = (): ImportRulesSchema => ({
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 getImportRulesSchemaDecodedMock = (): ImportRulesSchemaDecoded => ({
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',
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',
immutable: false,
});

View file

@ -0,0 +1,180 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
/* eslint-disable @typescript-eslint/camelcase */
import {
description,
anomaly_threshold,
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,
ThrottleOrNull,
note,
Version,
References,
Actions,
Enabled,
FalsePositives,
From,
Interval,
language,
query,
rule_id,
id,
created_at,
updated_at,
created_by,
updated_by,
} from '../common/schemas';
/* eslint-enable @typescript-eslint/camelcase */
import { DefaultStringArray } from '../types/default_string_array';
import { DefaultActionsArray } from '../types/default_actions_array';
import { DefaultBooleanTrue } from '../types/default_boolean_true';
import { DefaultFromString } from '../types/default_from_string';
import { DefaultIntervalString } from '../types/default_interval_string';
import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number';
import { DefaultToString } from '../types/default_to_string';
import { DefaultThreatArray } from '../types/default_threat_array';
import { DefaultThrottleNull } from '../types/default_throttle_null';
import { DefaultVersionNumber } from '../types/default_version_number';
import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array';
import { OnlyFalseAllowed } from '../types/only_false_allowed';
import { DefaultStringBooleanFalse } from '../types/default_string_boolean_false';
/**
* Differences from this and the createRulesSchema are
* - rule_id is required
* - id is optional (but ignored in the import code - rule_id is exclusively used for imports)
* - immutable is optional but if it is any value other than false it will be rejected
* - created_at is optional (but ignored in the import code)
* - updated_at is optional (but ignored in the import code)
* - created_by is optional (but ignored in the import code)
* - updated_by is optional (but ignored in the import code)
*/
export const importRulesSchema = t.intersection([
t.exact(
t.type({
description,
risk_score,
name,
severity,
type,
rule_id,
})
),
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
enabled: DefaultBooleanTrue, // defaults to true 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
index, // defaults to undefined if not set during decode
immutable: OnlyFalseAllowed, // defaults to "false" 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
// 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
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
throttle: DefaultThrottleNull, // defaults to "null" 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: ListsDefaultArray, // defaults to empty array if not set during decode
created_at, // defaults "undefined" if not set during decode
updated_at, // defaults "undefined" if not set during decode
created_by, // defaults "undefined" if not set during decode
updated_by, // defaults "undefined" if not set during decode
})
),
]);
export type ImportRulesSchema = t.TypeOf<typeof importRulesSchema>;
// This type is used after a decode since some things are defaults after a decode.
export type ImportRulesSchemaDecoded = Omit<
ImportRulesSchema,
| 'references'
| 'actions'
| 'enabled'
| 'false_positives'
| 'from'
| 'interval'
| 'max_signals'
| 'tags'
| 'to'
| 'threat'
| 'throttle'
| 'version'
| 'exceptions_list'
| 'rule_id'
| 'immutable'
> & {
references: References;
actions: Actions;
enabled: Enabled;
false_positives: FalsePositives;
from: From;
interval: Interval;
max_signals: MaxSignals;
tags: Tags;
to: To;
threat: Threat;
throttle: ThrottleOrNull;
version: Version;
exceptions_list: ListsDefaultArraySchema;
rule_id: RuleId;
immutable: false;
};
export const importRulesQuerySchema = t.exact(
t.partial({
overwrite: DefaultStringBooleanFalse,
})
);
export type ImportRulesQuerySchema = t.TypeOf<typeof importRulesQuerySchema>;
export type ImportRulesQuerySchemaDecoded = Omit<ImportRulesQuerySchema, 'overwrite'> & {
overwrite: boolean;
};
export const importRulesPayloadSchema = t.exact(
t.type({
file: t.object,
})
);
export type ImportRulesPayloadSchema = t.TypeOf<typeof importRulesPayloadSchema>;
export type ImportRulesPayloadSchemaDecoded = ImportRulesPayloadSchema;

View file

@ -0,0 +1,68 @@
/*
* 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 { getImportRulesSchemaMock } from './import_rules_schema.mock';
import { ImportRulesSchema } from './import_rules_schema';
import { importRuleValidateTypeDependents } from './import_rules_type_dependents';
describe('import_rules_type_dependents', () => {
test('saved_id is required when type is saved_query and will not validate without out', () => {
const schema: ImportRulesSchema = { ...getImportRulesSchemaMock(), type: 'saved_query' };
delete schema.saved_id;
const errors = importRuleValidateTypeDependents(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: ImportRulesSchema = {
...getImportRulesSchemaMock(),
type: 'saved_query',
saved_id: '123',
};
const errors = importRuleValidateTypeDependents(schema);
expect(errors).toEqual([]);
});
test('You cannot omit timeline_title when timeline_id is present', () => {
const schema: ImportRulesSchema = {
...getImportRulesSchemaMock(),
timeline_id: '123',
};
delete schema.timeline_title;
const errors = importRuleValidateTypeDependents(schema);
expect(errors).toEqual(['when "timeline_id" exists, "timeline_title" must also exist']);
});
test('You cannot have empty string for timeline_title when timeline_id is present', () => {
const schema: ImportRulesSchema = {
...getImportRulesSchemaMock(),
timeline_id: '123',
timeline_title: '',
};
const errors = importRuleValidateTypeDependents(schema);
expect(errors).toEqual(['"timeline_title" cannot be an empty string']);
});
test('You cannot have timeline_title with an empty timeline_id', () => {
const schema: ImportRulesSchema = {
...getImportRulesSchemaMock(),
timeline_id: '',
timeline_title: 'some-title',
};
const errors = importRuleValidateTypeDependents(schema);
expect(errors).toEqual(['"timeline_id" cannot be an empty string']);
});
test('You cannot have timeline_title without timeline_id', () => {
const schema: ImportRulesSchema = {
...getImportRulesSchemaMock(),
timeline_title: 'some-title',
};
delete schema.timeline_id;
const errors = importRuleValidateTypeDependents(schema);
expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']);
});
});

View file

@ -0,0 +1,105 @@
/*
* 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 { ImportRulesSchema } from './import_rules_schema';
export const validateAnomalyThreshold = (rule: ImportRulesSchema): string[] => {
if (rule.type === 'machine_learning') {
if (rule.anomaly_threshold == null) {
return ['when "type" is "machine_learning" anomaly_threshold is required'];
} else {
return [];
}
} else {
return [];
}
};
export const validateQuery = (rule: ImportRulesSchema): string[] => {
if (rule.type === 'machine_learning') {
if (rule.query != null) {
return ['when "type" is "machine_learning", "query" cannot be set'];
} else {
return [];
}
} else {
return [];
}
};
export const validateLanguage = (rule: ImportRulesSchema): string[] => {
if (rule.type === 'machine_learning') {
if (rule.language != null) {
return ['when "type" is "machine_learning", "language" cannot be set'];
} else {
return [];
}
} else {
return [];
}
};
export const validateSavedId = (rule: ImportRulesSchema): 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: ImportRulesSchema): string[] => {
if (rule.type === 'machine_learning') {
if (rule.machine_learning_job_id == null) {
return ['when "type" is "machine_learning", "machine_learning_job_id" is required'];
} else {
return [];
}
} else {
return [];
}
};
export const validateTimelineId = (rule: ImportRulesSchema): string[] => {
if (rule.timeline_id != null) {
if (rule.timeline_title == null) {
return ['when "timeline_id" exists, "timeline_title" must also exist'];
} else if (rule.timeline_id === '') {
return ['"timeline_id" cannot be an empty string'];
} else {
return [];
}
}
return [];
};
export const validateTimelineTitle = (rule: ImportRulesSchema): string[] => {
if (rule.timeline_title != null) {
if (rule.timeline_id == null) {
return ['when "timeline_title" exists, "timeline_id" must also exist'];
} else if (rule.timeline_title === '') {
return ['"timeline_title" cannot be an empty string'];
} else {
return [];
}
}
return [];
};
export const importRuleValidateTypeDependents = (schema: ImportRulesSchema): string[] => {
return [
...validateAnomalyThreshold(schema),
...validateQuery(schema),
...validateLanguage(schema),
...validateSavedId(schema),
...validateMachineLearningJobId(schema),
...validateTimelineId(schema),
...validateTimelineTitle(schema),
];
};

View file

@ -0,0 +1,81 @@
/*
* 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 { getPatchRulesSchemaMock } from './patch_rules_schema.mock';
import { PatchRulesSchema } from './patch_rules_schema';
import { patchRuleValidateTypeDependents } from './patch_rules_type_dependents';
describe('patch_rules_type_dependents', () => {
test('saved_id is required when type is saved_query and validates with it', () => {
const schema: PatchRulesSchema = {
...getPatchRulesSchemaMock(),
type: 'saved_query',
saved_id: '123',
};
const errors = patchRuleValidateTypeDependents(schema);
expect(errors).toEqual([]);
});
test('You cannot omit timeline_title when timeline_id is present', () => {
const schema: PatchRulesSchema = {
...getPatchRulesSchemaMock(),
timeline_id: '123',
};
delete schema.timeline_title;
const errors = patchRuleValidateTypeDependents(schema);
expect(errors).toEqual(['when "timeline_id" exists, "timeline_title" must also exist']);
});
test('You cannot have empty string for timeline_title when timeline_id is present', () => {
const schema: PatchRulesSchema = {
...getPatchRulesSchemaMock(),
timeline_id: '123',
timeline_title: '',
};
const errors = patchRuleValidateTypeDependents(schema);
expect(errors).toEqual(['"timeline_title" cannot be an empty string']);
});
test('You cannot have timeline_title with an empty timeline_id', () => {
const schema: PatchRulesSchema = {
...getPatchRulesSchemaMock(),
timeline_id: '',
timeline_title: 'some-title',
};
const errors = patchRuleValidateTypeDependents(schema);
expect(errors).toEqual(['"timeline_id" cannot be an empty string']);
});
test('You cannot have timeline_title without timeline_id', () => {
const schema: PatchRulesSchema = {
...getPatchRulesSchemaMock(),
timeline_title: 'some-title',
};
delete schema.timeline_id;
const errors = patchRuleValidateTypeDependents(schema);
expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']);
});
test('You cannot have both an id and a rule_id', () => {
const schema: PatchRulesSchema = {
...getPatchRulesSchemaMock(),
id: 'some-id',
rule_id: 'some-rule-id',
};
const errors = patchRuleValidateTypeDependents(schema);
expect(errors).toEqual(['both "id" and "rule_id" cannot exist, choose one or the other']);
});
test('You must set either an id or a rule_id', () => {
const schema: PatchRulesSchema = {
...getPatchRulesSchemaMock(),
};
delete schema.id;
delete schema.rule_id;
const errors = patchRuleValidateTypeDependents(schema);
expect(errors).toEqual(['either "id" or "rule_id" must be set']);
});
});

View file

@ -0,0 +1,101 @@
/*
* 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 { patchRulesBulkSchema, PatchRulesBulkSchema } from './patch_rules_bulk_schema';
import { exactCheck } from '../../../exact_check';
import { foldLeftRight } from '../../../test_utils';
import { formatErrors } from '../../../format_errors';
import { PatchRulesSchema } from './patch_rules_schema';
// only the basics of testing are here.
// see: patch_rules_schema.test.ts for the bulk of the validation tests
// this just wraps patchRulesSchema in an array
describe('patch_rules_bulk_schema', () => {
test('can take an empty array and validate it', () => {
const payload: PatchRulesBulkSchema = [];
const decoded = patchRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(output.errors).toEqual([]);
expect(output.schema).toEqual([]);
});
test('made up values do not validate for a single element', () => {
const payload: Array<{ madeUp: string }> = [{ madeUp: 'hi' }];
const decoded = patchRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUp"']);
expect(output.schema).toEqual({});
});
test('single array of [id] does validate', () => {
const payload: PatchRulesBulkSchema = [{ id: '4125761e-51da-4de9-a0c8-42824f532ddb' }];
const decoded = patchRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
});
test('two arrays of [id] validate', () => {
const payload: PatchRulesBulkSchema = [
{ id: '4125761e-51da-4de9-a0c8-42824f532ddb' },
{ id: '192f403d-b285-4251-9e8b-785fcfcf22e8' },
];
const decoded = patchRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
});
test('can set "note" to be a string', () => {
const payload: PatchRulesBulkSchema = [
{ id: '4125761e-51da-4de9-a0c8-42824f532ddb' },
{ note: 'hi' },
];
const decoded = patchRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
});
test('can set "note" to be an empty string', () => {
const payload: PatchRulesBulkSchema = [
{ id: '4125761e-51da-4de9-a0c8-42824f532ddb' },
{ note: '' },
];
const decoded = patchRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
});
test('cannot set "note" to be anything other than a string', () => {
const payload: Array<Omit<PatchRulesSchema, 'note'> & { note?: object }> = [
{ id: '4125761e-51da-4de9-a0c8-42824f532ddb' },
{ note: { someprop: 'some value here' } },
];
const decoded = patchRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
// TODO: Fix the formatter to give something better than [object Object]
expect(formatErrors(output.errors)).toEqual([
'Invalid value "[object Object]" supplied to "note"',
]);
expect(output.schema).toEqual({});
});
});

View file

@ -0,0 +1,14 @@
/*
* 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 { patchRulesSchema, PatchRulesSchemaDecoded } from './patch_rules_schema';
export const patchRulesBulkSchema = t.array(patchRulesSchema);
export type PatchRulesBulkSchema = t.TypeOf<typeof patchRulesBulkSchema>;
export type PatchRulesBulkSchemaDecoded = PatchRulesSchemaDecoded[];

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PatchRulesSchema, PatchRulesSchemaDecoded } from './patch_rules_schema';
export const getPatchRulesSchemaMock = (): PatchRulesSchema => ({
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 getPatchRulesSchemaDecodedMock = (): PatchRulesSchemaDecoded =>
getPatchRulesSchemaMock();

View file

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
/* eslint-disable @typescript-eslint/camelcase */
import {
description,
anomaly_threshold,
filters,
index,
output_index,
saved_id,
timeline_id,
timeline_title,
meta,
machine_learning_job_id,
risk_score,
rule_id,
name,
severity,
type,
note,
version,
actions,
false_positives,
interval,
max_signals,
from,
enabled,
tags,
threat,
throttle,
references,
to,
language,
listAndOrUndefined,
query,
id,
} from '../common/schemas';
/* eslint-enable @typescript-eslint/camelcase */
/**
* All of the patch elements should default to undefined if not set
*/
export const patchRulesSchema = t.exact(
t.partial({
description,
risk_score,
name,
severity,
type,
id,
actions,
anomaly_threshold,
enabled,
false_positives,
filters,
from,
rule_id,
index,
interval,
query,
language,
// TODO: output_index: This should be removed eventually
output_index,
saved_id,
timeline_id,
timeline_title,
meta,
machine_learning_job_id,
max_signals,
tags,
to,
threat,
throttle,
references,
note,
version,
exceptions_list: listAndOrUndefined,
})
);
export type PatchRulesSchema = t.TypeOf<typeof patchRulesSchema>;
// This type is used after a decode since some things are defaults after a decode.
export type PatchRulesSchemaDecoded = PatchRulesSchema;

View file

@ -0,0 +1,77 @@
/*
* 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 { PatchRulesSchema } from './patch_rules_schema';
export const validateQuery = (rule: PatchRulesSchema): string[] => {
if (rule.type === 'machine_learning') {
if (rule.query != null) {
return ['when "type" is "machine_learning", "query" cannot be set'];
} else {
return [];
}
} else {
return [];
}
};
export const validateLanguage = (rule: PatchRulesSchema): string[] => {
if (rule.type === 'machine_learning') {
if (rule.language != null) {
return ['when "type" is "machine_learning", "language" cannot be set'];
} else {
return [];
}
} else {
return [];
}
};
export const validateTimelineId = (rule: PatchRulesSchema): string[] => {
if (rule.timeline_id != null) {
if (rule.timeline_title == null) {
return ['when "timeline_id" exists, "timeline_title" must also exist'];
} else if (rule.timeline_id === '') {
return ['"timeline_id" cannot be an empty string'];
} else {
return [];
}
}
return [];
};
export const validateTimelineTitle = (rule: PatchRulesSchema): string[] => {
if (rule.timeline_title != null) {
if (rule.timeline_id == null) {
return ['when "timeline_title" exists, "timeline_id" must also exist'];
} else if (rule.timeline_title === '') {
return ['"timeline_title" cannot be an empty string'];
} else {
return [];
}
}
return [];
};
export const validateId = (rule: PatchRulesSchema): string[] => {
if (rule.id != null && rule.rule_id != null) {
return ['both "id" and "rule_id" cannot exist, choose one or the other'];
} else if (rule.id == null && rule.rule_id == null) {
return ['either "id" or "rule_id" must be set'];
} else {
return [];
}
};
export const patchRuleValidateTypeDependents = (schema: PatchRulesSchema): string[] => {
return [
...validateId(schema),
...validateQuery(schema),
...validateLanguage(schema),
...validateTimelineId(schema),
...validateTimelineTitle(schema),
];
};

View file

@ -0,0 +1,103 @@
/*
* 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 { queryRulesBulkSchema, QueryRulesBulkSchema } from './query_rules_bulk_schema';
import { exactCheck } from '../../../exact_check';
import { foldLeftRight } from '../../../test_utils';
import { formatErrors } from '../../../format_errors';
// only the basics of testing are here.
// see: query_rules_schema.test.ts for the bulk of the validation tests
// this just wraps queryRulesSchema in an array
describe('query_rules_bulk_schema', () => {
test('can take an empty array and validate it', () => {
const payload: QueryRulesBulkSchema = [];
const decoded = queryRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([]);
});
test('non uuid being supplied to id does not validate', () => {
const payload: QueryRulesBulkSchema = [
{
id: '1',
},
];
const decoded = queryRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['Invalid value "1" supplied to "id"']);
expect(output.schema).toEqual({});
});
test('both rule_id and id being supplied do validate', () => {
const payload: QueryRulesBulkSchema = [
{
rule_id: '1',
id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f',
},
];
const decoded = queryRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
});
test('only id validates with two elements', () => {
const payload: QueryRulesBulkSchema = [
{ id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' },
{ id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' },
];
const decoded = queryRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
});
test('only rule_id validates', () => {
const payload: QueryRulesBulkSchema = [{ rule_id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' }];
const decoded = queryRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
});
test('only rule_id validates with two elements', () => {
const payload: QueryRulesBulkSchema = [
{ rule_id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' },
{ rule_id: '2' },
];
const decoded = queryRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
});
test('both id and rule_id validates with two separate elements', () => {
const payload: QueryRulesBulkSchema = [
{ id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' },
{ rule_id: '2' },
];
const decoded = queryRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
});
});

View file

@ -0,0 +1,14 @@
/*
* 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 { queryRulesSchema, QueryRulesSchemaDecoded } from './query_rules_schema';
export const queryRulesBulkSchema = t.array(queryRulesSchema);
export type QueryRulesBulkSchema = t.TypeOf<typeof queryRulesBulkSchema>;
export type QueryRulesBulkSchemaDecoded = QueryRulesSchemaDecoded[];

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { queryRulesSchema, QueryRulesSchema } from './query_rules_schema';
import { exactCheck } from '../../../exact_check';
import { pipe } from 'fp-ts/lib/pipeable';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { left } from 'fp-ts/lib/Either';
describe('query_rules_schema', () => {
test('empty objects do validate', () => {
const payload: Partial<QueryRulesSchema> = {};
const decoded = queryRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({});
});
});

View file

@ -4,13 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Joi from 'joi';
import * as t from 'io-ts';
/* eslint-disable @typescript-eslint/camelcase */
import { rule_id, id } from './schemas';
import { rule_id, id } from '../common/schemas';
/* eslint-enable @typescript-eslint/camelcase */
export const queryRulesSchema = Joi.object({
rule_id,
id,
}).xor('id', 'rule_id');
export const queryRulesSchema = t.exact(
t.partial({
rule_id,
id,
})
);
export type QueryRulesSchema = t.TypeOf<typeof queryRulesSchema>;
export type QueryRulesSchemaDecoded = QueryRulesSchema;

View file

@ -0,0 +1,25 @@
/*
* 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 { QueryRulesSchema } from './query_rules_schema';
import { queryRuleValidateTypeDependents } from './query_rules_type_dependents';
describe('query_rules_type_dependents', () => {
test('You cannot have both an id and a rule_id', () => {
const schema: QueryRulesSchema = {
id: 'some-id',
rule_id: 'some-rule-id',
};
const errors = queryRuleValidateTypeDependents(schema);
expect(errors).toEqual(['both "id" and "rule_id" cannot exist, choose one or the other']);
});
test('You must set either an id or a rule_id', () => {
const schema: QueryRulesSchema = {};
const errors = queryRuleValidateTypeDependents(schema);
expect(errors).toEqual(['either "id" or "rule_id" must be set']);
});
});

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { QueryRulesSchema } from './query_rules_schema';
export const validateId = (rule: QueryRulesSchema): string[] => {
if (rule.id != null && rule.rule_id != null) {
return ['both "id" and "rule_id" cannot exist, choose one or the other'];
} else if (rule.id == null && rule.rule_id == null) {
return ['either "id" or "rule_id" must be set'];
} else {
return [];
}
};
export const queryRuleValidateTypeDependents = (schema: QueryRulesSchema): string[] => {
return [...validateId(schema)];
};

View file

@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/*
* 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 { QuerySignalsSchema, querySignalsSchema } from './query_signals_index_schema';
import { exactCheck } from '../../../exact_check';
import { pipe } from 'fp-ts/lib/pipeable';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { left } from 'fp-ts/lib/Either';
describe('query, aggs, size, _source and track_total_hits on signals index', () => {
test('query, aggs, size, _source and track_total_hits simultaneously', () => {
const payload: QuerySignalsSchema = {
query: {},
aggs: {},
size: 1,
track_total_hits: true,
_source: ['field'],
};
const decoded = querySignalsSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('query, only', () => {
const payload: QuerySignalsSchema = {
query: {},
};
const decoded = querySignalsSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('aggs only', () => {
const payload: QuerySignalsSchema = {
aggs: {},
};
const decoded = querySignalsSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('size only', () => {
const payload: QuerySignalsSchema = {
size: 1,
};
const decoded = querySignalsSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('track_total_hits only', () => {
const payload: QuerySignalsSchema = {
track_total_hits: true,
};
const decoded = querySignalsSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('_source only', () => {
const payload: QuerySignalsSchema = {
_source: ['field'],
};
const decoded = querySignalsSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
});

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { PositiveIntegerGreaterThanZero } from '../types/positive_integer_greater_than_zero';
export const querySignalsSchema = t.exact(
t.partial({
query: t.object,
aggs: t.object,
size: PositiveIntegerGreaterThanZero,
track_total_hits: t.boolean,
_source: t.array(t.string),
})
);
export type QuerySignalsSchema = t.TypeOf<typeof querySignalsSchema>;
export type QuerySignalsSchemaDecoded = QuerySignalsSchema;

View file

@ -0,0 +1,82 @@
/*
* 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 { setSignalsStatusSchema, SetSignalsStatusSchema } from './set_signal_status_schema';
import { exactCheck } from '../../../exact_check';
import { pipe } from 'fp-ts/lib/pipeable';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { left } from 'fp-ts/lib/Either';
describe('set signal status schema', () => {
test('signal_ids and status is valid', () => {
const payload: SetSignalsStatusSchema = {
signal_ids: ['somefakeid'],
status: 'open',
};
const decoded = setSignalsStatusSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('query and status is valid', () => {
const payload: SetSignalsStatusSchema = {
query: {},
status: 'open',
};
const decoded = setSignalsStatusSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('signal_ids and missing status is invalid', () => {
const payload: Omit<SetSignalsStatusSchema, 'status'> = {
signal_ids: ['somefakeid'],
};
const decoded = setSignalsStatusSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "status"',
]);
expect(message.schema).toEqual({});
});
test('query and missing status is invalid', () => {
const payload: Omit<SetSignalsStatusSchema, 'status'> = {
query: {},
};
const decoded = setSignalsStatusSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "status"',
]);
expect(message.schema).toEqual({});
});
test('signal_ids is present but status has wrong value', () => {
const payload: Omit<SetSignalsStatusSchema, 'status'> & { status: 'fakeVal' } = {
query: {},
status: 'fakeVal',
};
const decoded = setSignalsStatusSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "fakeVal" supplied to "status"',
]);
expect(message.schema).toEqual({});
});
});

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
/* eslint-disable @typescript-eslint/camelcase */
import { signal_ids, signal_status_query, status } from '../common/schemas';
/* eslint-enable @typescript-eslint/camelcase */
export const setSignalsStatusSchema = t.intersection([
t.type({
status,
}),
t.partial({
signal_ids,
query: signal_status_query,
}),
]);
export type SetSignalsStatusSchema = t.TypeOf<typeof setSignalsStatusSchema>;
export type SetSignalsStatusSchemaDecoded = SetSignalsStatusSchema;

View file

@ -0,0 +1,46 @@
/*
* 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 { setSignalStatusValidateTypeDependents } from './set_signal_status_type_dependents';
import { SetSignalsStatusSchema } from './set_signal_status_schema';
describe('update_rules_type_dependents', () => {
test('You can have just a "signals_id"', () => {
const schema: SetSignalsStatusSchema = {
status: 'open',
signal_ids: ['some-id'],
};
const errors = setSignalStatusValidateTypeDependents(schema);
expect(errors).toEqual([]);
});
test('You can have just a "query"', () => {
const schema: SetSignalsStatusSchema = {
status: 'open',
query: {},
};
const errors = setSignalStatusValidateTypeDependents(schema);
expect(errors).toEqual([]);
});
test('You cannot have both a "signals_id" and a "query"', () => {
const schema: SetSignalsStatusSchema = {
status: 'open',
query: {},
signal_ids: ['some-id'],
};
const errors = setSignalStatusValidateTypeDependents(schema);
expect(errors).toEqual(['both "signal_ids" and "query" cannot exist, choose one or the other']);
});
test('You must set either an "signals_id" and a "query"', () => {
const schema: SetSignalsStatusSchema = {
status: 'open',
};
const errors = setSignalStatusValidateTypeDependents(schema);
expect(errors).toEqual(['either "signal_ids" or "query" must be set']);
});
});

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SetSignalsStatusSchema } from './set_signal_status_schema';
export const validateId = (signalStatus: SetSignalsStatusSchema): string[] => {
if (signalStatus.signal_ids != null && signalStatus.query != null) {
return ['both "signal_ids" and "query" cannot exist, choose one or the other'];
} else if (signalStatus.signal_ids == null && signalStatus.query == null) {
return ['either "signal_ids" or "query" must be set'];
} else {
return [];
}
};
export const setSignalStatusValidateTypeDependents = (schema: SetSignalsStatusSchema): string[] => {
return [...validateId(schema)];
};

View file

@ -0,0 +1,277 @@
/*
* 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 { updateRulesBulkSchema, UpdateRulesBulkSchema } from './update_rules_bulk_schema';
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';
// only the basics of testing are here.
// see: update_rules_schema.test.ts for the bulk of the validation tests
// this just wraps updateRulesSchema in an array
describe('update_rules_bulk_schema', () => {
test('can take an empty array and validate it', () => {
const payload: UpdateRulesBulkSchema = [];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(output.errors).toEqual([]);
expect(output.schema).toEqual([]);
});
test('made up values do not validate for a single element', () => {
const payload: Array<{ madeUp: string }> = [{ madeUp: 'hi' }];
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(output.schema).toEqual({});
});
test('single array element does validate', () => {
const payload: UpdateRulesBulkSchema = [getUpdateRulesSchemaMock()];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([getUpdateRulesSchemaDecodedMock()]);
});
test('two array elements do validate', () => {
const payload: UpdateRulesBulkSchema = [getUpdateRulesSchemaMock(), getUpdateRulesSchemaMock()];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([
getUpdateRulesSchemaDecodedMock(),
getUpdateRulesSchemaDecodedMock(),
]);
});
test('single array element with a missing value (risk_score) will not validate', () => {
const singleItem = getUpdateRulesSchemaMock();
delete singleItem.risk_score;
const payload: UpdateRulesBulkSchema = [singleItem];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
});
test('two array elements where the first is valid but the second is invalid (risk_score) will not validate', () => {
const singleItem = getUpdateRulesSchemaMock();
const secondItem = getUpdateRulesSchemaMock();
delete secondItem.risk_score;
const payload: UpdateRulesBulkSchema = [singleItem, secondItem];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
});
test('two array elements where the first is invalid (risk_score) but the second is valid will not validate', () => {
const singleItem = getUpdateRulesSchemaMock();
const secondItem = getUpdateRulesSchemaMock();
delete singleItem.risk_score;
const payload: UpdateRulesBulkSchema = [singleItem, secondItem];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
});
test('two array elements where both are invalid (risk_score) will not validate', () => {
const singleItem = getUpdateRulesSchemaMock();
const secondItem = getUpdateRulesSchemaMock();
delete singleItem.risk_score;
delete secondItem.risk_score;
const payload: UpdateRulesBulkSchema = [singleItem, secondItem];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
});
test('two array elements where the first is invalid (extra key and value) but the second is valid will not validate', () => {
const singleItem: UpdateRulesSchema & { madeUpValue: string } = {
...getUpdateRulesSchemaMock(),
madeUpValue: 'something',
};
const secondItem = getUpdateRulesSchemaMock();
const payload: UpdateRulesBulkSchema = [singleItem, secondItem];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue"']);
expect(output.schema).toEqual({});
});
test('two array elements where the second is invalid (extra key and value) but the first is valid will not validate', () => {
const singleItem: UpdateRulesSchema = getUpdateRulesSchemaMock();
const secondItem: UpdateRulesSchema & { madeUpValue: string } = {
...getUpdateRulesSchemaMock(),
madeUpValue: 'something',
};
const payload: UpdateRulesBulkSchema = [singleItem, secondItem];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue"']);
expect(output.schema).toEqual({});
});
test('two array elements where both are invalid (extra key and value) will not validate', () => {
const singleItem: UpdateRulesSchema & { madeUpValue: string } = {
...getUpdateRulesSchemaMock(),
madeUpValue: 'something',
};
const secondItem: UpdateRulesSchema & { madeUpValue: string } = {
...getUpdateRulesSchemaMock(),
madeUpValue: 'something',
};
const payload: UpdateRulesBulkSchema = [singleItem, secondItem];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue,madeUpValue"']);
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];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['Invalid value "madeup" supplied to "severity"']);
expect(output.schema).toEqual({});
});
test('You can set "note" to a string', () => {
const payload: UpdateRulesBulkSchema = [
{ ...getUpdateRulesSchemaMock(), note: '# test markdown' },
];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([
{ ...getUpdateRulesSchemaDecodedMock(), note: '# test markdown' },
]);
});
test('You can set "note" to an empty string', () => {
const payload: UpdateRulesBulkSchema = [{ ...getUpdateRulesSchemaMock(), note: '' }];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([{ ...getUpdateRulesSchemaDecodedMock(), note: '' }]);
});
test('You can set "note" to anything other than string', () => {
const payload = [
{
...getUpdateRulesSchemaMock(),
note: {
something: 'some object',
},
},
];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
// TODO: We should change the formatter used to better print objects
expect(formatErrors(output.errors)).toEqual([
'Invalid value "[object Object]" supplied to "note"',
]);
expect(output.schema).toEqual({});
});
test('The default for "actions" will be an empty array', () => {
const { actions, ...withoutActions } = getUpdateRulesSchemaMock();
const payload: UpdateRulesBulkSchema = [withoutActions];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect((output.schema as UpdateRulesBulkSchema)[0].actions).toEqual([]);
});
test('The default for "throttle" will be null', () => {
const { throttle, ...withoutThrottle } = getUpdateRulesSchemaMock();
const payload: UpdateRulesBulkSchema = [withoutThrottle];
const decoded = updateRulesBulkSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect((output.schema as UpdateRulesBulkSchema)[0].throttle).toEqual(null);
});
});

View file

@ -0,0 +1,14 @@
/*
* 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 { updateRulesSchema, UpdateRulesSchemaDecoded } from './update_rules_schema';
export const updateRulesBulkSchema = t.array(updateRulesSchema);
export type UpdateRulesBulkSchema = t.TypeOf<typeof updateRulesBulkSchema>;
export type UpdateRulesBulkSchemaDecoded = UpdateRulesSchemaDecoded[];

View file

@ -0,0 +1,42 @@
/*
* 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 => ({
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',
references: [],
actions: [],
enabled: true,
false_positives: [],
from: 'now-6m',
interval: '5m',
max_signals: DEFAULT_MAX_SIGNALS,
tags: [],
to: 'now',
threat: [],
throttle: null,
exceptions_list: [],
rule_id: 'rule-1',
});

View file

@ -0,0 +1,140 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
/* eslint-disable @typescript-eslint/camelcase */
import {
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,
ThrottleOrNull,
note,
version,
References,
Actions,
Enabled,
FalsePositives,
From,
Interval,
language,
query,
id,
} from '../common/schemas';
/* eslint-enable @typescript-eslint/camelcase */
import { DefaultStringArray } from '../types/default_string_array';
import { DefaultActionsArray } from '../types/default_actions_array';
import { DefaultBooleanTrue } from '../types/default_boolean_true';
import { DefaultFromString } from '../types/default_from_string';
import { DefaultIntervalString } from '../types/default_interval_string';
import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number';
import { DefaultToString } from '../types/default_to_string';
import { DefaultThreatArray } from '../types/default_threat_array';
import { DefaultThrottleNull } from '../types/default_throttle_null';
import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array';
/**
* 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
enabled: DefaultBooleanTrue, // defaults to true 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, // 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
// 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
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
throttle: DefaultThrottleNull, // defaults to "null" 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: ListsDefaultArray, // defaults to empty array 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,
| 'references'
| 'actions'
| 'enabled'
| 'false_positives'
| 'from'
| 'interval'
| 'max_signals'
| 'tags'
| 'to'
| 'threat'
| 'throttle'
| 'exceptions_list'
| 'rule_id'
> & {
references: References;
actions: Actions;
enabled: Enabled;
false_positives: FalsePositives;
from: From;
interval: Interval;
max_signals: MaxSignals;
tags: Tags;
to: To;
threat: Threat;
throttle: ThrottleOrNull;
exceptions_list: ListsDefaultArraySchema;
rule_id: RuleId;
};

View file

@ -0,0 +1,88 @@
/*
* 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 { getUpdateRulesSchemaMock } from './update_rules_schema.mock';
import { UpdateRulesSchema } from './update_rules_schema';
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(),
timeline_id: '123',
};
delete schema.timeline_title;
const errors = updateRuleValidateTypeDependents(schema);
expect(errors).toEqual(['when "timeline_id" exists, "timeline_title" must also exist']);
});
test('You cannot have empty string for timeline_title when timeline_id is present', () => {
const schema: UpdateRulesSchema = {
...getUpdateRulesSchemaMock(),
timeline_id: '123',
timeline_title: '',
};
const errors = updateRuleValidateTypeDependents(schema);
expect(errors).toEqual(['"timeline_title" cannot be an empty string']);
});
test('You cannot have timeline_title with an empty timeline_id', () => {
const schema: UpdateRulesSchema = {
...getUpdateRulesSchemaMock(),
timeline_id: '',
timeline_title: 'some-title',
};
const errors = updateRuleValidateTypeDependents(schema);
expect(errors).toEqual(['"timeline_id" cannot be an empty string']);
});
test('You cannot have timeline_title without timeline_id', () => {
const schema: UpdateRulesSchema = {
...getUpdateRulesSchemaMock(),
timeline_title: 'some-title',
};
delete schema.timeline_id;
const errors = updateRuleValidateTypeDependents(schema);
expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']);
});
test('You cannot have both an id and a rule_id', () => {
const schema: UpdateRulesSchema = {
...getUpdateRulesSchemaMock(),
id: 'some-id',
rule_id: 'some-rule-id',
};
const errors = updateRuleValidateTypeDependents(schema);
expect(errors).toEqual(['both "id" and "rule_id" cannot exist, choose one or the other']);
});
test('You must set either an id or a rule_id', () => {
const schema: UpdateRulesSchema = {
...getUpdateRulesSchemaMock(),
};
delete schema.id;
delete schema.rule_id;
const errors = updateRuleValidateTypeDependents(schema);
expect(errors).toEqual(['either "id" or "rule_id" must be set']);
});
});

View file

@ -0,0 +1,116 @@
/*
* 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 } from './update_rules_schema';
export const validateAnomalyThreshold = (rule: UpdateRulesSchema): string[] => {
if (rule.type === 'machine_learning') {
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 (rule.type === 'machine_learning') {
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 (rule.type === 'machine_learning') {
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 (rule.type === 'machine_learning') {
if (rule.machine_learning_job_id == null) {
return ['when "type" is "machine_learning", "machine_learning_job_id" is required'];
} else {
return [];
}
} else {
return [];
}
};
export const validateTimelineId = (rule: UpdateRulesSchema): string[] => {
if (rule.timeline_id != null) {
if (rule.timeline_title == null) {
return ['when "timeline_id" exists, "timeline_title" must also exist'];
} else if (rule.timeline_id === '') {
return ['"timeline_id" cannot be an empty string'];
} else {
return [];
}
}
return [];
};
export const validateTimelineTitle = (rule: UpdateRulesSchema): string[] => {
if (rule.timeline_title != null) {
if (rule.timeline_id == null) {
return ['when "timeline_title" exists, "timeline_id" must also exist'];
} else if (rule.timeline_title === '') {
return ['"timeline_title" cannot be an empty string'];
} else {
return [];
}
}
return [];
};
export const validateId = (rule: UpdateRulesSchema): string[] => {
if (rule.id != null && rule.rule_id != null) {
return ['both "id" and "rule_id" cannot exist, choose one or the other'];
} else if (rule.id == null && rule.rule_id == null) {
return ['either "id" or "rule_id" must be set'];
} else {
return [];
}
};
export const updateRuleValidateTypeDependents = (schema: UpdateRulesSchema): string[] => {
return [
...validateId(schema),
...validateAnomalyThreshold(schema),
...validateQuery(schema),
...validateLanguage(schema),
...validateSavedId(schema),
...validateMachineLearningJobId(schema),
...validateTimelineId(schema),
...validateTimelineTitle(schema),
];
};

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { DefaultBooleanTrue } from './default_boolean_true';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
describe('default_boolean_true', () => {
test('it should validate a boolean false', () => {
const payload = false;
const decoded = DefaultBooleanTrue.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a boolean true', () => {
const payload = true;
const decoded = DefaultBooleanTrue.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultBooleanTrue.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default true', () => {
const payload = null;
const decoded = DefaultBooleanTrue.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(true);
});
});

View file

@ -0,0 +1,39 @@
/*
* 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 { DefaultFromString } from './default_from_string';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
describe('default_from_string', () => {
test('it should validate a from string', () => {
const payload = 'now-20m';
const decoded = DefaultFromString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultFromString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default of "now-6m"', () => {
const payload = null;
const decoded = DefaultFromString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('now-6m');
});
});

View file

@ -0,0 +1,54 @@
/*
* 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 { DefaultActionsArray } from './default_actions_array';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { Actions } from '../common/schemas';
describe('default_actions_array', () => {
test('it should validate an empty array', () => {
const payload: string[] = [];
const decoded = DefaultActionsArray.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 actions', () => {
const payload: Actions = [
{ id: '123', group: 'group', action_type_id: 'action_type_id', params: {} },
];
const decoded = DefaultActionsArray.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 = [
{ id: '123', group: 'group', action_type_id: 'action_type_id', params: {} },
5,
];
const decoded = DefaultActionsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default array entry', () => {
const payload = null;
const decoded = DefaultActionsArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([]);
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { actions, Actions } from '../common/schemas';
/**
* Types the DefaultStringArray as:
* - If null or undefined, then a default action array will be set
*/
export const DefaultActionsArray = new t.Type<Actions, Actions, unknown>(
'DefaultActionsArray',
actions.is,
(input): Either<t.Errors, Actions> => (input == null ? t.success([]) : actions.decode(input)),
t.identity
);
export type DefaultActionsArrayC = typeof DefaultActionsArray;

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { DefaultBooleanFalse } from './default_boolean_false';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
describe('default_boolean_false', () => {
test('it should validate a boolean false', () => {
const payload = false;
const decoded = DefaultBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a boolean true', () => {
const payload = true;
const decoded = DefaultBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default false', () => {
const payload = null;
const decoded = DefaultBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(false);
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultBooleanFalse as:
* - If null or undefined, then a default false will be set
*/
export const DefaultBooleanFalse = new t.Type<boolean, boolean, unknown>(
'DefaultBooleanFalse',
t.boolean.is,
(input): Either<t.Errors, boolean> =>
input == null ? t.success(false) : t.boolean.decode(input),
t.identity
);
export type DefaultBooleanFalseC = typeof DefaultBooleanFalse;

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultBooleanTrue as:
* - If null or undefined, then a default true will be set
*/
export const DefaultBooleanTrue = new t.Type<boolean, boolean, unknown>(
'DefaultBooleanTrue',
t.boolean.is,
(input): Either<t.Errors, boolean> => (input == null ? t.success(true) : t.boolean.decode(input)),
t.identity
);
export type DefaultBooleanTrueC = typeof DefaultBooleanTrue;

View file

@ -0,0 +1,39 @@
/*
* 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 { DefaultEmptyString } from './default_empty_string';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
describe('default_empty_string', () => {
test('it should validate a regular string', () => {
const payload = 'some string';
const decoded = DefaultEmptyString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultEmptyString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default of ""', () => {
const payload = null;
const decoded = DefaultEmptyString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('');
});
});

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultEmptyString as:
* - If null or undefined, then a default of an empty string "" will be used
*/
export const DefaultEmptyString = new t.Type<string, string, unknown>(
'DefaultEmptyString',
t.string.is,
(input): Either<t.Errors, string> => (input == null ? t.success('') : t.string.decode(input)),
t.identity
);
export type DefaultEmptyStringC = typeof DefaultEmptyString;

View file

@ -0,0 +1,39 @@
/*
* 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 { DefaultExportFileName } from './default_export_file_name';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
describe('default_export_file_name', () => {
test('it should validate a regular string', () => {
const payload = 'some string';
const decoded = DefaultExportFileName.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultExportFileName.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default of "export.ndjson"', () => {
const payload = null;
const decoded = DefaultExportFileName.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('export.ndjson');
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultExportFileName as:
* - If null or undefined, then a default of "export.ndjson" will be used
*/
export const DefaultExportFileName = new t.Type<string, string, unknown>(
'DefaultExportFileName',
t.string.is,
(input): Either<t.Errors, string> =>
input == null ? t.success('export.ndjson') : t.string.decode(input),
t.identity
);
export type DefaultExportFileNameC = typeof DefaultExportFileName;

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultFromString as:
* - If null or undefined, then a default of the string "now-6m" will be used
*/
export const DefaultFromString = new t.Type<string, string, unknown>(
'DefaultFromString',
t.string.is,
(input): Either<t.Errors, string> =>
input == null ? t.success('now-6m') : t.string.decode(input),
t.identity
);
export type DefaultFromStringC = typeof DefaultFromString;

View file

@ -0,0 +1,39 @@
/*
* 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 { DefaultIntervalString } from './default_interval_string';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
describe('default_interval_string', () => {
test('it should validate a interval string', () => {
const payload = '20m';
const decoded = DefaultIntervalString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultIntervalString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default of "5m"', () => {
const payload = null;
const decoded = DefaultIntervalString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('5m');
});
});

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultIntervalString as:
* - If null or undefined, then a default of the string "5m" will be used
*/
export const DefaultIntervalString = new t.Type<string, string, unknown>(
'DefaultIntervalString',
t.string.is,
(input): Either<t.Errors, string> => (input == null ? t.success('5m') : t.string.decode(input)),
t.identity
);
export type DefaultIntervalStringC = typeof DefaultIntervalString;

View file

@ -0,0 +1,40 @@
/*
* 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 { DefaultLanguageString } from './default_language_string';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { Language } from '../common/schemas';
describe('default_language_string', () => {
test('it should validate a string', () => {
const payload: Language = 'lucene';
const decoded = DefaultLanguageString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultLanguageString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default of "kuery"', () => {
const payload = null;
const decoded = DefaultLanguageString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('kuery');
});
});

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { language } from '../common/schemas';
/**
* Types the DefaultLanguageString as:
* - If null or undefined, then a default of the string "kuery" will be used
*/
export const DefaultLanguageString = new t.Type<string, string, unknown>(
'DefaultLanguageString',
t.string.is,
(input): Either<t.Errors, string> =>
input == null ? t.success('kuery') : language.decode(input),
t.identity
);
export type DefaultLanguageStringC = typeof DefaultLanguageString;

View file

@ -0,0 +1,58 @@
/*
* 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 { DefaultMaxSignalsNumber } from './default_max_signals_number';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { DEFAULT_MAX_SIGNALS } from '../../../constants';
describe('default_from_string', () => {
test('it should validate a max signal number', () => {
const payload = 5;
const decoded = DefaultMaxSignalsNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a string', () => {
const payload = '5';
const decoded = DefaultMaxSignalsNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should not validate a zero', () => {
const payload = 0;
const decoded = DefaultMaxSignalsNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should not validate a negative number', () => {
const payload = -1;
const decoded = DefaultMaxSignalsNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default of DEFAULT_MAX_SIGNALS', () => {
const payload = null;
const decoded = DefaultMaxSignalsNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(DEFAULT_MAX_SIGNALS);
});
});

View file

@ -0,0 +1,32 @@
/*
* 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';
// eslint-disable-next-line @typescript-eslint/camelcase
import { max_signals } from '../common/schemas';
import { DEFAULT_MAX_SIGNALS } from '../../../constants';
/**
* Types the default max signal:
* - Natural Number (positive integer and not a float),
* - greater than 1
* - If undefined then it will use DEFAULT_MAX_SIGNALS (100) as the default
*/
export const DefaultMaxSignalsNumber: DefaultMaxSignalsNumberC = new t.Type<
number,
number,
unknown
>(
'DefaultMaxSignals',
t.number.is,
(input): Either<t.Errors, number> => {
return input == null ? t.success(DEFAULT_MAX_SIGNALS) : max_signals.decode(input);
},
t.identity
);
export type DefaultMaxSignalsNumberC = t.Type<number, number, unknown>;

View file

@ -0,0 +1,75 @@
/*
* 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 { DefaultPage } from './default_page';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
describe('default_page', () => {
test('it should validate a regular number greater than zero', () => {
const payload = 5;
const decoded = DefaultPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a string of a number', () => {
const payload = '5';
const decoded = DefaultPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(5);
});
test('it should not validate a junk string', () => {
const payload = 'invalid-string';
const decoded = DefaultPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "NaN" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should not validate an empty string', () => {
const payload = '';
const decoded = DefaultPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "NaN" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should not validate a zero', () => {
const payload = 0;
const decoded = DefaultPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should not validate a negative number', () => {
const payload = -1;
const decoded = DefaultPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default of 20', () => {
const payload = null;
const decoded = DefaultPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(1);
});
});

View file

@ -0,0 +1,32 @@
/*
* 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';
import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero';
/**
* Types the DefaultPerPage as:
* - If a string this will convert the string to a number
* - If null or undefined, then a default of 1 will be used
* - If the number is 0 or less this will not validate as it has to be a positive number greater than zero
*/
export const DefaultPage = new t.Type<number, number, unknown>(
'DefaultPerPage',
t.number.is,
(input): Either<t.Errors, number> => {
if (input == null) {
return t.success(1);
} else if (typeof input === 'string') {
return PositiveIntegerGreaterThanZero.decode(parseInt(input, 10));
} else {
return PositiveIntegerGreaterThanZero.decode(input);
}
},
t.identity
);
export type DefaultPageC = typeof DefaultPage;

View file

@ -0,0 +1,75 @@
/*
* 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 { DefaultPerPage } from './default_per_page';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
describe('default_per_page', () => {
test('it should validate a regular number greater than zero', () => {
const payload = 5;
const decoded = DefaultPerPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a string of a number', () => {
const payload = '5';
const decoded = DefaultPerPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(5);
});
test('it should not validate a junk string', () => {
const payload = 'invalid-string';
const decoded = DefaultPerPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "NaN" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should not validate an empty string', () => {
const payload = '';
const decoded = DefaultPerPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "NaN" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should not validate a zero', () => {
const payload = 0;
const decoded = DefaultPerPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should not validate a negative number', () => {
const payload = -1;
const decoded = DefaultPerPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default of 20', () => {
const payload = null;
const decoded = DefaultPerPage.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(20);
});
});

View file

@ -0,0 +1,32 @@
/*
* 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';
import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero';
/**
* Types the DefaultPerPage as:
* - If a string this will convert the string to a number
* - If null or undefined, then a default of 20 will be used
* - If the number is 0 or less this will not validate as it has to be a positive number greater than zero
*/
export const DefaultPerPage = new t.Type<number, number, unknown>(
'DefaultPerPage',
t.number.is,
(input): Either<t.Errors, number> => {
if (input == null) {
return t.success(20);
} else if (typeof input === 'string') {
return PositiveIntegerGreaterThanZero.decode(parseInt(input, 10));
} else {
return PositiveIntegerGreaterThanZero.decode(input);
}
},
t.identity
);
export type DefaultPerPageC = typeof DefaultPerPage;

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { DefaultStringArray } from './default_string_array';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
describe('default_string_array', () => {
test('it should validate an empty array', () => {
const payload: string[] = [];
const decoded = DefaultStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate an array of strings', () => {
const payload = ['value 1', 'value 2'];
const decoded = DefaultStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate an array with a number', () => {
const payload = ['value 1', 5];
const decoded = DefaultStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default array entry', () => {
const payload = null;
const decoded = DefaultStringArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([]);
});
});

View file

@ -7,16 +7,16 @@
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
export type DefaultStringArrayC = t.Type<string[], string[], unknown>;
/**
* Types the DefaultStringArray as:
* - If null or undefined, then a default array will be set
*/
export const DefaultStringArray: DefaultStringArrayC = new t.Type<string[], string[], unknown>(
'DefaultArray',
export const DefaultStringArray = new t.Type<string[], string[], unknown>(
'DefaultStringArray',
t.array(t.string).is,
(input): Either<t.Errors, string[]> =>
input == null ? t.success([]) : t.array(t.string).decode(input),
t.identity
);
export type DefaultStringArrayC = typeof DefaultStringArray;

View file

@ -0,0 +1,93 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { DefaultStringBooleanFalse } from './default_string_boolean_false';
describe('default_string_boolean_false', () => {
test('it should validate a boolean false', () => {
const payload = false;
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should validate a boolean true', () => {
const payload = true;
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default false', () => {
const payload = null;
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(false);
});
test('it should return a default false when given a string of "false"', () => {
const payload = 'false';
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(false);
});
test('it should return a default true when given a string of "true"', () => {
const payload = 'true';
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(true);
});
test('it should return a default true when given a string of "TruE"', () => {
const payload = 'TruE';
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(true);
});
test('it should not work with a strong of junk "junk"', () => {
const payload = 'junk';
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "junk" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should not work with an empty string', () => {
const payload = '';
const decoded = DefaultStringBooleanFalse.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to ""']);
expect(message.schema).toEqual({});
});
});

View file

@ -0,0 +1,32 @@
/*
* 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';
/**
* Types the DefaultStringBooleanFalse as:
* - If a string this will convert the string to a boolean
* - If null or undefined, then a default false will be set
*/
export const DefaultStringBooleanFalse = new t.Type<boolean, boolean, unknown>(
'DefaultStringBooleanFalse',
t.boolean.is,
(input): Either<t.Errors, boolean> => {
if (input == null) {
return t.success(false);
} else if (typeof input === 'string' && input.toLowerCase() === 'true') {
return t.success(true);
} else if (typeof input === 'string' && input.toLowerCase() === 'false') {
return t.success(false);
} else {
return t.boolean.decode(input);
}
},
t.identity
);
export type DefaultStringBooleanFalseC = typeof DefaultStringBooleanFalse;

View file

@ -0,0 +1,62 @@
/*
* 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 { DefaultThreatArray } from './default_threat_array';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { Threat } from '../common/schemas';
describe('default_threat_null', () => {
test('it should validate an empty array', () => {
const payload: Threat = [];
const decoded = DefaultThreatArray.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 threats', () => {
const payload: Threat = [
{
framework: 'MITRE ATTACK',
technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }],
tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' },
},
];
const decoded = DefaultThreatArray.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 = [
{
framework: 'MITRE ATTACK',
technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }],
tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' },
},
5,
];
const decoded = DefaultThreatArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default empty array if not provided a value', () => {
const payload = null;
const decoded = DefaultThreatArray.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([]);
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { Threat, threat } from '../common/schemas';
/**
* Types the DefaultThreatArray as:
* - If null or undefined, then an empty array will be set
*/
export const DefaultThreatArray = new t.Type<Threat, Threat, unknown>(
'DefaultThreatArray',
threat.is,
(input): Either<t.Errors, Threat> => (input == null ? t.success([]) : threat.decode(input)),
t.identity
);
export type DefaultThreatArrayC = typeof DefaultThreatArray;

View file

@ -0,0 +1,40 @@
/*
* 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 { DefaultThrottleNull } from './default_throttle_null';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { Throttle } from '../common/schemas';
describe('default_throttle_null', () => {
test('it should validate a throttle string', () => {
const payload: Throttle = 'some string';
const decoded = DefaultThrottleNull.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 = 5;
const decoded = DefaultThrottleNull.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default "null" if not provided a value', () => {
const payload = undefined;
const decoded = DefaultThrottleNull.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(null);
});
});

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { ThrottleOrNull, throttle } from '../common/schemas';
/**
* Types the DefaultThrottleNull as:
* - If null or undefined, then a null will be set
*/
export const DefaultThrottleNull = new t.Type<ThrottleOrNull, ThrottleOrNull, unknown>(
'DefaultThreatNull',
throttle.is,
(input): Either<t.Errors, ThrottleOrNull> =>
input == null ? t.success(null) : throttle.decode(input),
t.identity
);
export type DefaultThrottleNullC = typeof DefaultThrottleNull;

View file

@ -0,0 +1,39 @@
/*
* 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 { DefaultToString } from './default_to_string';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
describe('default_to_string', () => {
test('it should validate a to string', () => {
const payload = 'now-5m';
const decoded = DefaultToString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultToString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default of "now"', () => {
const payload = null;
const decoded = DefaultToString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('now');
});
});

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
/**
* Types the DefaultToString as:
* - If null or undefined, then a default of the string "now" will be used
*/
export const DefaultToString = new t.Type<string, string, unknown>(
'DefaultFromString',
t.string.is,
(input): Either<t.Errors, string> => (input == null ? t.success('now') : t.string.decode(input)),
t.identity
);
export type DefaultToStringC = typeof DefaultToString;

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { DefaultUuid } from './default_uuid';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
describe('default_uuid', () => {
test('it should validate a regular string', () => {
const payload = '1';
const decoded = DefaultUuid.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = DefaultUuid.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default of a uuid', () => {
const payload = null;
const decoded = DefaultUuid.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i
);
});
});

View file

@ -10,17 +10,17 @@ import uuid from 'uuid';
import { NonEmptyString } from './non_empty_string';
export type DefaultUuidC = t.Type<string, string, unknown>;
/**
* Types the DefaultUuid as:
* - If null or undefined, then a default string uuid.v4() will be
* created otherwise it will be checked just against an empty string
*/
export const DefaultUuid: DefaultUuidC = new t.Type<string, string, unknown>(
export const DefaultUuid = new t.Type<string, string, unknown>(
'DefaultUuid',
t.string.is,
(input): Either<t.Errors, string> =>
input == null ? t.success(uuid.v4()) : NonEmptyString.decode(input),
t.identity
);
export type DefaultUuidC = typeof DefaultUuid;

View file

@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { DefaultVersionNumber } from './default_version_number';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
describe('default_version_number', () => {
test('it should validate a version number', () => {
const payload = 5;
const decoded = DefaultVersionNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a 0', () => {
const payload = 0;
const decoded = DefaultVersionNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "0" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should not validate a -1', () => {
const payload = -1;
const decoded = DefaultVersionNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should not validate a string', () => {
const payload = '5';
const decoded = DefaultVersionNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default of 1', () => {
const payload = null;
const decoded = DefaultVersionNumber.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(1);
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
import { version, Version } from '../common/schemas';
/**
* Types the DefaultVersionNumber as:
* - If null or undefined, then a default of the number 1 will be used
*/
export const DefaultVersionNumber = new t.Type<Version, Version, unknown>(
'DefaultVersionNumber',
version.is,
(input): Either<t.Errors, Version> => (input == null ? t.success(1) : version.decode(input)),
t.identity
);
export type DefaultVersionNumberC = typeof DefaultVersionNumber;

View file

@ -7,13 +7,11 @@
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
export type IsoDateStringC = t.Type<string, string, unknown>;
/**
* Types the IsoDateString as:
* - A string that is an ISOString
*/
export const IsoDateString: IsoDateStringC = new t.Type<string, string, unknown>(
export const IsoDateString = new t.Type<string, string, unknown>(
'IsoDateString',
t.string.is,
(input, context): Either<t.Errors, string> => {
@ -34,3 +32,5 @@ export const IsoDateString: IsoDateStringC = new t.Type<string, string, unknown>
},
t.identity
);
export type IsoDateStringC = typeof IsoDateString;

View file

@ -13,7 +13,6 @@ import {
list_values_operator as listOperator,
} from '../common/schemas';
export type ListsDefaultArrayC = t.Type<List[], List[], unknown>;
export type List = t.TypeOf<typeof listAnd>;
export type ListValues = t.TypeOf<typeof listValues>;
export type ListOperator = t.TypeOf<typeof listOperator>;
@ -22,7 +21,7 @@ export type ListOperator = t.TypeOf<typeof listOperator>;
* Types the ListsDefaultArray as:
* - If null or undefined, then a default array will be set for the list
*/
export const ListsDefaultArray: ListsDefaultArrayC = new t.Type<List[], List[], unknown>(
export const ListsDefaultArray = new t.Type<List[], List[], unknown>(
'listsWithDefaultArray',
t.array(listAnd).is,
(input): Either<t.Errors, List[]> =>
@ -30,4 +29,6 @@ export const ListsDefaultArray: ListsDefaultArrayC = new t.Type<List[], List[],
t.identity
);
export type ListsDefaultArrayC = typeof ListsDefaultArray;
export type ListsDefaultArraySchema = t.TypeOf<typeof ListsDefaultArray>;

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { NonEmptyString } from './non_empty_string';
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
describe('non_empty_string', () => {
test('it should validate a regular string', () => {
const payload = '1';
const decoded = NonEmptyString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = NonEmptyString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should not validate an empty string', () => {
const payload = '';
const decoded = NonEmptyString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should not validate empty spaces', () => {
const payload = ' ';
const decoded = NonEmptyString.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value " " supplied to ""']);
expect(message.schema).toEqual({});
});
});

View file

@ -7,13 +7,11 @@
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
export type NonEmptyStringC = t.Type<string, string, unknown>;
/**
* Types the NonEmptyString as:
* - A string that is not empty
*/
export const NonEmptyString: NonEmptyStringC = new t.Type<string, string, unknown>(
export const NonEmptyString = new t.Type<string, string, unknown>(
'NonEmptyString',
t.string.is,
(input, context): Either<t.Errors, string> => {
@ -25,3 +23,5 @@ export const NonEmptyString: NonEmptyStringC = new t.Type<string, string, unknow
},
t.identity
);
export type NonEmptyStringC = typeof NonEmptyString;

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { OnlyFalseAllowed } from './only_false_allowed';
describe('only_false_allowed', () => {
test('it should validate a boolean false as false', () => {
const payload = false;
const decoded = OnlyFalseAllowed.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
test('it should not validate a boolean true', () => {
const payload = true;
const decoded = OnlyFalseAllowed.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "true" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should not validate a number', () => {
const payload = 5;
const decoded = OnlyFalseAllowed.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']);
expect(message.schema).toEqual({});
});
test('it should return a default false', () => {
const payload = null;
const decoded = OnlyFalseAllowed.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(false);
});
});

View file

@ -0,0 +1,33 @@
/*
* 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';
/**
* Types the OnlyFalseAllowed as:
* - If null or undefined, then a default false will be set
* - If true is sent in then this will return an error
* - If false is sent in then this will allow it only false
*/
export const OnlyFalseAllowed = new t.Type<boolean, boolean, unknown>(
'DefaultBooleanTrue',
t.boolean.is,
(input, context): Either<t.Errors, boolean> => {
if (input == null) {
return t.success(false);
} else {
if (typeof input === 'boolean' && input === false) {
return t.success(false);
} else {
return t.failure(input, context);
}
}
},
t.identity
);
export type OnlyFalseAllowedC = typeof OnlyFalseAllowed;

View file

@ -7,14 +7,12 @@
import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';
export type PositiveIntegerC = t.Type<number, number, unknown>;
/**
* Types the positive integer are:
* - Natural Number (positive integer and not a float),
* - zero or greater
*/
export const PositiveInteger: PositiveIntegerC = new t.Type<number, number, unknown>(
export const PositiveInteger = new t.Type<number, number, unknown>(
'PositiveInteger',
t.number.is,
(input, context): Either<t.Errors, number> => {
@ -24,3 +22,5 @@ export const PositiveInteger: PositiveIntegerC = new t.Type<number, number, unkn
},
t.identity
);
export type PositiveIntegerC = typeof PositiveInteger;

Some files were not shown because too many files have changed in this diff Show more