mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
e49888f2ec
commit
d99cf75814
230 changed files with 13701 additions and 10845 deletions
|
@ -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>;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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',
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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),
|
||||
];
|
||||
};
|
File diff suppressed because it is too large
Load diff
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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[];
|
|
@ -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',
|
||||
});
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
};
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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),
|
||||
];
|
||||
};
|
|
@ -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({});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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({});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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)];
|
||||
};
|
|
@ -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,
|
||||
});
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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),
|
||||
];
|
||||
};
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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({});
|
||||
});
|
||||
});
|
|
@ -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[];
|
|
@ -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();
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
|
@ -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),
|
||||
];
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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[];
|
|
@ -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({});
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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)];
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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({});
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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)];
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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[];
|
|
@ -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',
|
||||
});
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
};
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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),
|
||||
];
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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([]);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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;
|
|
@ -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('');
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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;
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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>;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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([]);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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({});
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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([]);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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({});
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue