diff --git a/docs/accessibility.asciidoc b/docs/accessibility.asciidoc
new file mode 100644
index 00000000000000..4869d35dab1568
--- /dev/null
+++ b/docs/accessibility.asciidoc
@@ -0,0 +1,65 @@
+[chapter]
+[[accessibility]]
+= Accessibility Statement for Kibana
+++++
+Accessibility
+++++
+
+Elastic is committed to ensuring digital accessibility for people with disabilities. We are continually improving the user experience, and strive toward ensuring our tools are usable by everyone.
+
+[float]
+[[accessibility-measures]]
+== Measures to support accessibility
+Elastic takes the following measures to ensure accessibility of Kibana:
+
+* Maintains and incorporates an https://elastic.github.io/eui/[accessible component library].
+* Provides continual accessibility training for our staff.
+* Employs a third-party audit.
+
+[float]
+[[accessibility-conformance-status]]
+== Conformance status
+Kibana aims to meet https://www.w3.org/WAI/WCAG21/quickref/?currentsidebar=%23col_customize&levels=aaa&technologies=server%2Csmil%2Cflash%2Csl[WCAG 2.1 level AA] compliance. Currently, we can only claim to partially conform, meaning we do not fully meet all of the success criteria. However, we do try to take a broader view of accessibility, and go above and beyond the legal and regulatory standards to provide a good experience for all of our users.
+
+[float]
+[[accessibility-feedback]]
+== Feedback
+We welcome your feedback on the accessibility of Kibana. Please let us know if you encounter accessibility barriers on Kibana by either emailing us at accessibility@elastic.co or opening https://github.com/elastic/kibana/issues/new?labels=Project%3AAccessibility&template=Accessibility.md&title=%28Accessibility%29[an issue on GitHub].
+
+[float]
+[[accessibility-specs]]
+== Technical specifications
+Accessibility of Kibana relies on the following technologies to work with your web browser and any assistive technologies or plugins installed on your computer:
+
+* HTML
+* CSS
+* JavaScript
+* WAI-ARIA
+
+[float]
+[[accessibility-limitations-and-alternatives]]
+== Limitations and alternatives
+Despite our best efforts to ensure accessibility of Kibana, there are some limitations. Please https://github.com/elastic/kibana/issues/new?labels=Project%3AAccessibility&template=Accessibility.md&title=%28Accessibility%29[open an issue on GitHub] if you observe an issue not in this list.
+
+Known limitations are in the following areas:
+
+* *Charts*: We have a clear plan for the first steps of making charts accessible. We’ve opened this https://github.com/elastic/elastic-charts/issues/300[Charts accessibility ticket on GitHub] for tracking our progress.
+* *Maps*: Maps might pose difficulties to users with vision disabilities. We welcome your input on making our maps accessible. Go to the https://github.com/elastic/kibana/issues/57271[Maps accessibility ticket on GitHub] to join the discussion and view our plans.
+* *Tables*: Although generally accessible and marked-up as standard HTML tables with column headers, tables rarely make use of row headers and have poor captions. You will see incremental improvements as various applications adopt a new accessible component.
+* *Color contrast*: Modern Kibana interfaces generally do not have color contrast issues. However, older code might fall below the recommended contrast levels. As we continue to update our code, this issue will phase out naturally.
+
+To see individual tickets, view our https://github.com/elastic/kibana/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3AProject%3AAccessibility[GitHub issues with label "`Project:Accessibility`"].
+
+[float]
+[[accessibility-approach]]
+== Assessment approach
+Elastic assesses the accessibility of Kibana with the following approaches:
+
+* *Self-evaluation*: Our employees are familiar with accessibility standards and review new designs and implemented features to confirm that they are accessible.
+* *External evaluation*: We engage external contractors to help us conduct an independent assessment and generate a formal VPAT. Please email accessibility@elastic.co if you’d like a copy.
+* *Automated evaluation*: We are starting to run https://www.deque.com/axe/[axe] on every page. See our current progress in the https://github.com/elastic/kibana/issues/51456[automated testing GitHub issue].
+
+Manual testing largely focuses on screen reader support and is done on:
+
+* VoiceOver on MacOS with Safari, Chrome and Edge
+* NVDA on Windows with Chrome and Firefox
diff --git a/docs/index.asciidoc b/docs/index.asciidoc
index 491a9629e983e6..5474772ab7da81 100644
--- a/docs/index.asciidoc
+++ b/docs/index.asciidoc
@@ -22,6 +22,8 @@ include::{asciidoc-dir}/../../shared/attributes.asciidoc[]
include::user/index.asciidoc[]
+include::accessibility.asciidoc[]
+
include::limitations.asciidoc[]
include::release-notes/highlights.asciidoc[]
diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js
index f3e401bedcef33..eec75033e8beb0 100644
--- a/packages/kbn-pm/dist/index.js
+++ b/packages/kbn-pm/dist/index.js
@@ -88442,7 +88442,7 @@ module.exports = function kindOf(val) {
};
function ctorName(val) {
- return val.constructor ? val.constructor.name : null;
+ return typeof val.constructor === 'function' ? val.constructor.name : null;
}
function isArray(val) {
@@ -88653,7 +88653,7 @@ module.exports = function kindOf(val) {
};
function ctorName(val) {
- return val.constructor ? val.constructor.name : null;
+ return typeof val.constructor === 'function' ? val.constructor.name : null;
}
function isArray(val) {
@@ -88844,7 +88844,7 @@ module.exports = function kindOf(val) {
};
function ctorName(val) {
- return val.constructor ? val.constructor.name : null;
+ return typeof val.constructor === 'function' ? val.constructor.name : null;
}
function isArray(val) {
@@ -101921,7 +101921,7 @@ module.exports = function kindOf(val) {
};
function ctorName(val) {
- return val.constructor ? val.constructor.name : null;
+ return typeof val.constructor === 'function' ? val.constructor.name : null;
}
function isArray(val) {
@@ -104780,7 +104780,7 @@ module.exports = function kindOf(val) {
};
function ctorName(val) {
- return val.constructor ? val.constructor.name : null;
+ return typeof val.constructor === 'function' ? val.constructor.name : null;
}
function isArray(val) {
diff --git a/x-pack/legacy/plugins/alerting/common/types.ts b/x-pack/legacy/plugins/alerting/common/alert.ts
similarity index 94%
rename from x-pack/legacy/plugins/alerting/common/types.ts
rename to x-pack/legacy/plugins/alerting/common/alert.ts
index 54bf04d0765d63..8f28c8fbaed7f7 100644
--- a/x-pack/legacy/plugins/alerting/common/types.ts
+++ b/x-pack/legacy/plugins/alerting/common/alert.ts
@@ -5,12 +5,13 @@
*/
import { SavedObjectAttributes } from 'kibana/server';
-import { AlertActionParams } from '../server/types';
export interface IntervalSchedule extends SavedObjectAttributes {
interval: string;
}
+export type AlertActionParams = SavedObjectAttributes;
+
export interface AlertAction {
group: string;
id: string;
diff --git a/x-pack/legacy/plugins/alerting/common/alert_instance.ts b/x-pack/legacy/plugins/alerting/common/alert_instance.ts
new file mode 100644
index 00000000000000..a6852f06efd347
--- /dev/null
+++ b/x-pack/legacy/plugins/alerting/common/alert_instance.ts
@@ -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';
+import { DateFromString } from './date_from_string';
+
+const metaSchema = t.partial({
+ lastScheduledActions: t.type({
+ group: t.string,
+ date: DateFromString,
+ }),
+});
+export type AlertInstanceMeta = t.TypeOf;
+
+const stateSchema = t.record(t.string, t.unknown);
+export type AlertInstanceState = t.TypeOf;
+
+export const rawAlertInstance = t.partial({
+ state: stateSchema,
+ meta: metaSchema,
+});
+export type RawAlertInstance = t.TypeOf;
diff --git a/x-pack/legacy/plugins/alerting/common/alert_task_instance.ts b/x-pack/legacy/plugins/alerting/common/alert_task_instance.ts
new file mode 100644
index 00000000000000..50722a471f3d78
--- /dev/null
+++ b/x-pack/legacy/plugins/alerting/common/alert_task_instance.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { rawAlertInstance } from './alert_instance';
+import { DateFromString } from './date_from_string';
+
+export const alertStateSchema = t.partial({
+ alertTypeState: t.record(t.string, t.unknown),
+ alertInstances: t.record(t.string, rawAlertInstance),
+ previousStartedAt: t.union([t.null, DateFromString]),
+});
+
+export type AlertTaskState = t.TypeOf;
+
+export const alertParamsSchema = t.intersection([
+ t.type({
+ alertId: t.string,
+ }),
+ t.partial({
+ spaceId: t.string,
+ }),
+]);
+export type AlertTaskParams = t.TypeOf;
diff --git a/x-pack/legacy/plugins/alerting/common/date_from_string.test.ts b/x-pack/legacy/plugins/alerting/common/date_from_string.test.ts
new file mode 100644
index 00000000000000..ecf7bdb3245785
--- /dev/null
+++ b/x-pack/legacy/plugins/alerting/common/date_from_string.test.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { DateFromString } from './date_from_string';
+import { right, isLeft } from 'fp-ts/lib/Either';
+
+describe('DateFromString', () => {
+ test('validated and parses a string into a Date', () => {
+ const date = new Date(1973, 10, 30);
+ expect(DateFromString.decode(date.toISOString())).toEqual(right(date));
+ });
+
+ test('validated and returns a failure for an actual Date', () => {
+ const date = new Date(1973, 10, 30);
+ expect(isLeft(DateFromString.decode(date))).toEqual(true);
+ });
+
+ test('validated and returns a failure for an invalid Date string', () => {
+ expect(isLeft(DateFromString.decode('1234-23-45'))).toEqual(true);
+ });
+
+ test('validated and returns a failure for a null value', () => {
+ expect(isLeft(DateFromString.decode(null))).toEqual(true);
+ });
+});
diff --git a/x-pack/legacy/plugins/alerting/common/date_from_string.ts b/x-pack/legacy/plugins/alerting/common/date_from_string.ts
new file mode 100644
index 00000000000000..831891fc12d92b
--- /dev/null
+++ b/x-pack/legacy/plugins/alerting/common/date_from_string.ts
@@ -0,0 +1,26 @@
+/*
+ * 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';
+
+// represents a Date from an ISO string
+export const DateFromString = new t.Type(
+ 'DateFromString',
+ // detect the type
+ (value): value is Date => value instanceof Date,
+ (valueToDecode, context) =>
+ either.chain(
+ // validate this is a string
+ t.string.validate(valueToDecode, context),
+ // decode
+ value => {
+ const decoded = new Date(value);
+ return isNaN(decoded.getTime()) ? t.failure(valueToDecode, context) : t.success(decoded);
+ }
+ ),
+ valueToEncode => valueToEncode.toISOString()
+);
diff --git a/x-pack/legacy/plugins/alerting/common/index.ts b/x-pack/legacy/plugins/alerting/common/index.ts
index 9f4141dbcae7df..03b3487f10f1d9 100644
--- a/x-pack/legacy/plugins/alerting/common/index.ts
+++ b/x-pack/legacy/plugins/alerting/common/index.ts
@@ -4,4 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export * from './types';
+export * from './alert';
+export * from './alert_instance';
+export * from './alert_task_instance';
diff --git a/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.ts b/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.ts
index df67f7d2a1d9ee..4d106178f86fb4 100644
--- a/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.ts
+++ b/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.ts
@@ -3,10 +3,14 @@
* 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 {
+ AlertInstanceMeta,
+ AlertInstanceState,
+ RawAlertInstance,
+ rawAlertInstance,
+} from '../../common';
import { State, Context } from '../types';
-import { DateFromString } from '../lib/types';
import { parseDuration } from '../lib';
interface ScheduledExecutionOptions {
@@ -14,24 +18,7 @@ interface ScheduledExecutionOptions {
context: Context;
state: State;
}
-
-const metaSchema = t.partial({
- lastScheduledActions: t.type({
- group: t.string,
- date: DateFromString,
- }),
-});
-type AlertInstanceMeta = t.TypeOf;
-
-const stateSchema = t.record(t.string, t.unknown);
-type AlertInstanceState = t.TypeOf;
-
-export const rawAlertInstance = t.partial({
- state: stateSchema,
- meta: metaSchema,
-});
-export type RawAlertInstance = t.TypeOf;
-
+export type AlertInstances = Record;
export class AlertInstance {
private scheduledExecutionOptions?: ScheduledExecutionOptions;
private meta: AlertInstanceMeta;
diff --git a/x-pack/legacy/plugins/alerting/server/alert_instance/index.ts b/x-pack/legacy/plugins/alerting/server/alert_instance/index.ts
index fc828096adf284..40ee0874e805cd 100644
--- a/x-pack/legacy/plugins/alerting/server/alert_instance/index.ts
+++ b/x-pack/legacy/plugins/alerting/server/alert_instance/index.ts
@@ -4,5 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { AlertInstance, RawAlertInstance, rawAlertInstance } from './alert_instance';
+export { AlertInstance } from './alert_instance';
export { createAlertInstanceFactory } from './create_alert_instance_factory';
diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts
index 709c6d3faefba0..1875ac04ddaa79 100644
--- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts
+++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts
@@ -22,6 +22,7 @@ import {
AlertType,
IntervalSchedule,
SanitizedAlert,
+ AlertTaskState,
} from './types';
import { validateAlertTypeParams } from './lib';
import {
@@ -31,7 +32,7 @@ import {
} from '../../../../plugins/security/server';
import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server';
import { TaskManagerStartContract } from '../../../../plugins/task_manager/server';
-import { AlertTaskState, taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance';
+import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance';
type NormalizedAlertAction = Omit;
export type CreateAPIKeyResult =
diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.ts b/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.ts
index 33b416fe8e2da2..6bc318070377d3 100644
--- a/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.ts
+++ b/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.ts
@@ -7,32 +7,12 @@ import * as t from 'io-ts';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { ConcreteTaskInstance } from '../../../../../plugins/task_manager/server';
-import { SanitizedAlert } from '../types';
-import { DateFromString } from '../lib/types';
-import { AlertInstance, rawAlertInstance } from '../alert_instance';
+import { SanitizedAlert, AlertTaskState, alertParamsSchema, alertStateSchema } from '../../common';
export interface AlertTaskInstance extends ConcreteTaskInstance {
state: AlertTaskState;
}
-export const alertStateSchema = t.partial({
- alertTypeState: t.record(t.string, t.unknown),
- alertInstances: t.record(t.string, rawAlertInstance),
- previousStartedAt: t.union([t.null, DateFromString]),
-});
-export type AlertInstances = Record;
-export type AlertTaskState = t.TypeOf;
-
-const alertParamsSchema = t.intersection([
- t.type({
- alertId: t.string,
- }),
- t.partial({
- spaceId: t.string,
- }),
-]);
-export type AlertTaskParams = t.TypeOf;
-
const enumerateErrorFields = (e: t.Errors) =>
`${e.map(({ context }) => context.map(({ key }) => key).join('.'))}`;
diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts
index 1466d3ccd274b5..2632decb125f06 100644
--- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts
+++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts
@@ -10,16 +10,21 @@ import { SavedObject } from '../../../../../../src/core/server';
import { TaskRunnerContext } from './task_runner_factory';
import { ConcreteTaskInstance } from '../../../../../plugins/task_manager/server';
import { createExecutionHandler } from './create_execution_handler';
-import { AlertInstance, createAlertInstanceFactory, RawAlertInstance } from '../alert_instance';
+import { AlertInstance, createAlertInstanceFactory } from '../alert_instance';
import { getNextRunAt } from './get_next_run_at';
import { validateAlertTypeParams } from '../lib';
-import { AlertType, RawAlert, IntervalSchedule, Services, AlertInfoParams } from '../types';
-import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type';
import {
+ AlertType,
+ RawAlert,
+ IntervalSchedule,
+ Services,
+ AlertInfoParams,
+ RawAlertInstance,
AlertTaskState,
- AlertInstances,
- taskInstanceToAlertTaskInstance,
-} from './alert_task_instance';
+} from '../types';
+import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type';
+import { taskInstanceToAlertTaskInstance } from './alert_task_instance';
+import { AlertInstances } from '../alert_instance/alert_instance';
const FALLBACK_RETRY_INTERVAL: IntervalSchedule = { interval: '5m' };
diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts
index fd87292ef9147b..44381dc279dd25 100644
--- a/x-pack/legacy/plugins/alerting/server/types.ts
+++ b/x-pack/legacy/plugins/alerting/server/types.ts
@@ -8,8 +8,7 @@ import { AlertInstance } from './alert_instance';
import { AlertTypeRegistry as OrigAlertTypeRegistry } from './alert_type_registry';
import { PluginSetupContract, PluginStartContract } from './plugin';
import { SavedObjectAttributes, SavedObjectsClientContract } from '../../../../../src/core/server';
-import { Alert } from '../common';
-
+import { Alert, AlertActionParams } from '../common';
export * from '../common';
export type State = Record;
@@ -57,8 +56,6 @@ export interface AlertType {
executor: ({ services, params, state }: AlertExecutorOptions) => Promise;
}
-export type AlertActionParams = SavedObjectAttributes;
-
export interface RawAlertAction extends SavedObjectAttributes {
group: string;
actionRef: string;
diff --git a/x-pack/legacy/plugins/canvas/server/plugin.ts b/x-pack/legacy/plugins/canvas/server/plugin.ts
index 713747551ff478..014ff244e6e0c6 100644
--- a/x-pack/legacy/plugins/canvas/server/plugin.ts
+++ b/x-pack/legacy/plugins/canvas/server/plugin.ts
@@ -6,40 +6,9 @@
import { CoreSetup, PluginsSetup } from './shim';
import { functions } from '../canvas_plugin_src/functions/server';
-import { loadSampleData } from './sample_data';
export class Plugin {
public setup(core: CoreSetup, plugins: PluginsSetup) {
plugins.interpreter.register({ serverFunctions: functions });
-
- plugins.features.registerFeature({
- id: 'canvas',
- name: 'Canvas',
- icon: 'canvasApp',
- navLinkId: 'canvas',
- app: ['canvas', 'kibana'],
- catalogue: ['canvas'],
- privileges: {
- all: {
- savedObject: {
- all: ['canvas-workpad', 'canvas-element'],
- read: ['index-pattern'],
- },
- ui: ['save', 'show'],
- },
- read: {
- savedObject: {
- all: [],
- read: ['index-pattern', 'canvas-workpad', 'canvas-element'],
- },
- ui: ['show'],
- },
- },
- });
-
- loadSampleData(
- plugins.home.sampleData.addSavedObjectsToSampleDataset,
- plugins.home.sampleData.addAppLinksToSampleDataset
- );
}
}
diff --git a/x-pack/legacy/plugins/canvas/server/shim.ts b/x-pack/legacy/plugins/canvas/server/shim.ts
index c9d70e6a721ee7..c36ee3a291dae3 100644
--- a/x-pack/legacy/plugins/canvas/server/shim.ts
+++ b/x-pack/legacy/plugins/canvas/server/shim.ts
@@ -29,10 +29,6 @@ export interface PluginsSetup {
kibana: {
injectedUiAppVars: ReturnType;
};
- sampleData: {
- addSavedObjectsToSampleDataset: any;
- addAppLinksToSampleDataset: any;
- };
usageCollection: UsageCollectionSetup;
}
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts
index da98944d5f0c97..dfd812251e3d66 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts
@@ -139,7 +139,7 @@ export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise(
`${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
{
- method: 'PUT',
+ method: 'PATCH',
body: JSON.stringify(ids.map(id => ({ id, enabled }))),
asResponse: true,
}
@@ -160,7 +160,7 @@ export const deleteRules = async ({ ids }: DeleteRulesProps): Promise(
`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`,
{
- method: 'PUT',
+ method: 'DELETE',
body: JSON.stringify(ids.map(id => ({ id }))),
asResponse: true,
}
diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx
index e29e2ed193f946..69848c08fa3f88 100644
--- a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx
+++ b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx
@@ -36,6 +36,8 @@ export const throwIfNotOk = async (response?: Response): Promise => {
if (body != null && body.message) {
if (body.statusCode != null) {
throw new ToasterErrors([body.message, `${i18n.STATUS_CODE} ${body.statusCode}`]);
+ } else if (body.status_code != null) {
+ throw new ToasterErrors([body.message, `${i18n.STATUS_CODE} ${body.status_code}`]);
} else {
throw new ToasterErrors([body.message]);
}
diff --git a/x-pack/legacy/plugins/siem/public/utils/api/index.ts b/x-pack/legacy/plugins/siem/public/utils/api/index.ts
index 1dc14413b04d21..3c70083136505d 100644
--- a/x-pack/legacy/plugins/siem/public/utils/api/index.ts
+++ b/x-pack/legacy/plugins/siem/public/utils/api/index.ts
@@ -8,6 +8,7 @@ export interface MessageBody {
error?: string;
message?: string;
statusCode?: number;
+ status_code?: number;
}
export const parseJsonFromBody = async (response: Response): Promise => {
diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts
index a488db3f0c3d73..bab7936005c04a 100644
--- a/x-pack/legacy/plugins/siem/server/kibana.index.ts
+++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts
@@ -13,7 +13,7 @@ import { readIndexRoute } from './lib/detection_engine/routes/index/read_index_r
import { readRulesRoute } from './lib/detection_engine/routes/rules/read_rules_route';
import { findRulesRoute } from './lib/detection_engine/routes/rules/find_rules_route';
import { deleteRulesRoute } from './lib/detection_engine/routes/rules/delete_rules_route';
-import { updateRulesRoute } from './lib/detection_engine/routes/rules/update_rules_route';
+import { patchRulesRoute } from './lib/detection_engine/routes/rules/patch_rules_route';
import { setSignalsStatusRoute } from './lib/detection_engine/routes/signals/open_close_signals_route';
import { querySignalsRoute } from './lib/detection_engine/routes/signals/query_signals_route';
import { ServerFacade } from './types';
@@ -23,12 +23,14 @@ import { readTagsRoute } from './lib/detection_engine/routes/tags/read_tags_rout
import { readPrivilegesRoute } from './lib/detection_engine/routes/privileges/read_privileges_route';
import { addPrepackedRulesRoute } from './lib/detection_engine/routes/rules/add_prepackaged_rules_route';
import { createRulesBulkRoute } from './lib/detection_engine/routes/rules/create_rules_bulk_route';
-import { updateRulesBulkRoute } from './lib/detection_engine/routes/rules/update_rules_bulk_route';
+import { patchRulesBulkRoute } from './lib/detection_engine/routes/rules/patch_rules_bulk_route';
import { deleteRulesBulkRoute } from './lib/detection_engine/routes/rules/delete_rules_bulk_route';
import { importRulesRoute } from './lib/detection_engine/routes/rules/import_rules_route';
import { exportRulesRoute } from './lib/detection_engine/routes/rules/export_rules_route';
import { findRulesStatusesRoute } from './lib/detection_engine/routes/rules/find_rules_status_route';
import { getPrepackagedRulesStatusRoute } from './lib/detection_engine/routes/rules/get_prepackaged_rules_status_route';
+import { updateRulesRoute } from './lib/detection_engine/routes/rules/update_rules_route';
+import { updateRulesBulkRoute } from './lib/detection_engine/routes/rules/update_rules_bulk_route';
const APP_ID = 'siem';
@@ -50,12 +52,14 @@ export const initServerWithKibana = (context: PluginInitializerContext, __legacy
updateRulesRoute(__legacy);
deleteRulesRoute(__legacy);
findRulesRoute(__legacy);
+ patchRulesRoute(__legacy);
addPrepackedRulesRoute(__legacy);
getPrepackagedRulesStatusRoute(__legacy);
createRulesBulkRoute(__legacy);
updateRulesBulkRoute(__legacy);
deleteRulesBulkRoute(__legacy);
+ patchRulesBulkRoute(__legacy);
importRulesRoute(__legacy);
exportRulesRoute(__legacy);
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts
index 19c4279e06b032..b008ead8df9480 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts
@@ -108,6 +108,14 @@ export const getUpdateRequest = (): ServerInjectOptions => ({
},
});
+export const getPatchRequest = (): ServerInjectOptions => ({
+ method: 'PATCH',
+ url: DETECTION_ENGINE_RULES_URL,
+ payload: {
+ ...typicalPayload(),
+ },
+});
+
export const getReadRequest = (): ServerInjectOptions => ({
method: 'GET',
url: `${DETECTION_ENGINE_RULES_URL}?rule_id=rule-1`,
@@ -130,6 +138,12 @@ export const getUpdateBulkRequest = (): ServerInjectOptions => ({
payload: [typicalPayload()],
});
+export const getPatchBulkRequest = (): ServerInjectOptions => ({
+ method: 'PATCH',
+ url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
+ payload: [typicalPayload()],
+});
+
export const getDeleteBulkRequest = (): ServerInjectOptions => ({
method: 'DELETE',
url: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`,
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts
index 0eb090179b1925..e0d48836013ec0 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts
@@ -5,7 +5,6 @@
*/
import Hapi from 'hapi';
-import Boom from 'boom';
import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants';
import signalsPolicy from './signals_policy.json';
@@ -31,13 +30,18 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute =
},
},
},
- async handler(request: RequestFacade) {
+ async handler(request: RequestFacade, headers) {
try {
const index = getIndex(request, server);
const callWithRequest = callWithRequestFactory(request, server);
const indexExists = await getIndexExists(callWithRequest, index);
if (indexExists) {
- return new Boom(`index: "${index}" already exists`, { statusCode: 409 });
+ return headers
+ .response({
+ message: `index: "${index}" already exists`,
+ status_code: 409,
+ })
+ .code(409);
} else {
const policyExists = await getPolicyExists(callWithRequest, index);
if (!policyExists) {
@@ -52,7 +56,13 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute =
return { acknowledged: true };
}
} catch (err) {
- return transformError(err);
+ const error = transformError(err);
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
}
},
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts
index 82fe0f55215fbd..c1edc824b81eb6 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts
@@ -5,7 +5,6 @@
*/
import Hapi from 'hapi';
-import Boom from 'boom';
import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants';
import { ServerFacade, RequestFacade } from '../../../../types';
@@ -39,13 +38,18 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute =
},
},
},
- async handler(request: RequestFacade) {
+ async handler(request: RequestFacade, headers) {
try {
const index = getIndex(request, server);
const callWithRequest = callWithRequestFactory(request, server);
const indexExists = await getIndexExists(callWithRequest, index);
if (!indexExists) {
- return new Boom(`index: "${index}" does not exist`, { statusCode: 404 });
+ return headers
+ .response({
+ message: `index: "${index}" does not exist`,
+ status_code: 404,
+ })
+ .code(404);
} else {
await deleteAllIndex(callWithRequest, `${index}-*`);
const policyExists = await getPolicyExists(callWithRequest, index);
@@ -59,7 +63,13 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute =
return { acknowledged: true };
}
} catch (err) {
- return transformError(err);
+ const error = transformError(err);
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
}
},
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts
index a8c4b7407c4487..1a5018d446747e 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts
@@ -5,7 +5,6 @@
*/
import Hapi from 'hapi';
-import Boom from 'boom';
import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants';
import { ServerFacade, RequestFacade } from '../../../../types';
@@ -42,11 +41,22 @@ export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute =>
if (request.method.toLowerCase() === 'head') {
return headers.response().code(404);
} else {
- return new Boom('index for this space does not exist', { statusCode: 404 });
+ return headers
+ .response({
+ message: 'index for this space does not exist',
+ status_code: 404,
+ })
+ .code(404);
}
}
} catch (err) {
- return transformError(err);
+ const error = transformError(err);
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
}
},
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts
index 5ea4dc7595b2b1..45ecb7dc972884 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts
@@ -24,7 +24,7 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve
},
},
},
- async handler(request: RulesRequest) {
+ async handler(request: RulesRequest, headers) {
try {
const callWithRequest = callWithRequestFactory(request, server);
const index = getIndex(request, server);
@@ -35,7 +35,13 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve
has_encryption_key: !usingEphemeralEncryptionKey,
});
} catch (err) {
- return transformError(err);
+ const error = transformError(err);
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
}
},
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts
index e4f612a14832b1..ec86de84ff3c79 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts
@@ -85,10 +85,9 @@ describe('add_prepackaged_rules_route', () => {
alertsClient.create.mockResolvedValue(getResult());
const { payload } = await server.inject(addPrepackagedRulesRequest());
expect(JSON.parse(payload)).toEqual({
- error: 'Bad Request',
message:
'Pre-packaged rules cannot be installed until the space index is created: .siem-signals-default',
- statusCode: 400,
+ status_code: 400,
});
});
});
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts
index 28af530272bc77..e796f21d9c03a2 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts
@@ -6,7 +6,6 @@
import Hapi from 'hapi';
import { isFunction } from 'lodash/fp';
-import Boom from 'boom';
import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants';
import { ServerFacade, RequestFacade } from '../../../../types';
@@ -56,9 +55,12 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR
if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) {
const spaceIndexExists = await getIndexExists(callWithRequest, spaceIndex);
if (!spaceIndexExists) {
- return Boom.badRequest(
- `Pre-packaged rules cannot be installed until the space index is created: ${spaceIndex}`
- );
+ return headers
+ .response({
+ message: `Pre-packaged rules cannot be installed until the space index is created: ${spaceIndex}`,
+ status_code: 400,
+ })
+ .code(400);
}
}
await Promise.all(
@@ -76,7 +78,13 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR
rules_updated: rulesToUpdate.length,
};
} catch (err) {
- return transformError(err);
+ const error = transformError(err);
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
}
},
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts
index 27575fb264f7b4..e51634c0d2c072 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts
@@ -73,9 +73,8 @@ describe('create_rules', () => {
alertsClient.create.mockResolvedValue(getResult());
const { payload } = await server.inject(getCreateRequest());
expect(JSON.parse(payload)).toEqual({
- error: 'Bad Request',
message: 'To create a rule, the index must exist first. Index .siem-signals does not exist',
- statusCode: 400,
+ status_code: 400,
});
});
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts
index ec1df238f94838..de874f66d0444b 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts
@@ -6,7 +6,6 @@
import Hapi from 'hapi';
import { isFunction } from 'lodash/fp';
-import Boom from 'boom';
import uuid from 'uuid';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { createRules } from '../../rules/create_rules';
@@ -15,7 +14,7 @@ import { createRulesSchema } from '../schemas/create_rules_schema';
import { ServerFacade } from '../../../../types';
import { readRules } from '../../rules/read_rules';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
-import { transformOrError } from './utils';
+import { transform } from './utils';
import { getIndexExists } from '../../index/get_index_exists';
import { callWithRequestFactory, getIndex, transformError } from '../utils';
import { KibanaRequest } from '../../../../../../../../../src/core/server';
@@ -76,14 +75,22 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
const callWithRequest = callWithRequestFactory(request, server);
const indexExists = await getIndexExists(callWithRequest, finalIndex);
if (!indexExists) {
- return Boom.badRequest(
- `To create a rule, the index must exist first. Index ${finalIndex} does not exist`
- );
+ return headers
+ .response({
+ message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`,
+ status_code: 400,
+ })
+ .code(400);
}
if (ruleId != null) {
const rule = await readRules({ alertsClient, ruleId });
if (rule != null) {
- return Boom.conflict(`rule_id: "${ruleId}" already exists`);
+ return headers
+ .response({
+ message: `rule_id: "${ruleId}" already exists`,
+ status_code: 409,
+ })
+ .code(409);
}
}
const createdRule = await createRules({
@@ -126,9 +133,25 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
search: `${createdRule.id}`,
searchFields: ['alertId'],
});
- return transformOrError(createdRule, ruleStatuses.saved_objects[0]);
+ const transformed = transform(createdRule, ruleStatuses.saved_objects[0]);
+ if (transformed == null) {
+ return headers
+ .response({
+ message: 'Internal error transforming rules',
+ status_code: 500,
+ })
+ .code(500);
+ } else {
+ return transformed;
+ }
} catch (err) {
- return transformError(err);
+ const error = transformError(err);
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
}
},
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts
index c2b5576c091835..b3f8eafa24115f 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts
@@ -41,7 +41,7 @@ export const createDeleteRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
if (!alertsClient || !savedObjectsClient) {
return headers.response().code(404);
}
- const rules = Promise.all(
+ const rules = await Promise.all(
request.payload.map(async payloadRule => {
const { id, rule_id: ruleId } = payloadRule;
const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)';
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts
index 33f181cfbb5a5c..e4d3787c900721 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts
@@ -11,7 +11,7 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { deleteRules } from '../../rules/delete_rules';
import { ServerFacade } from '../../../../types';
import { queryRulesSchema } from '../schemas/query_rules_schema';
-import { getIdError, transformOrError } from './utils';
+import { getIdError, transform } from './utils';
import { transformError } from '../utils';
import { QueryRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
@@ -62,12 +62,34 @@ export const createDeleteRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
ruleStatuses.saved_objects.forEach(async obj =>
savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id)
);
- return transformOrError(rule, ruleStatuses.saved_objects[0]);
+ const transformed = transform(rule, ruleStatuses.saved_objects[0]);
+ if (transformed == null) {
+ return headers
+ .response({
+ message: 'Internal error transforming rules',
+ status_code: 500,
+ })
+ .code(500);
+ } else {
+ return transformed;
+ }
} else {
- return getIdError({ id, ruleId });
+ const error = getIdError({ id, ruleId });
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
}
} catch (err) {
- return transformError(err);
+ const error = transformError(err);
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
}
},
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts
index ce624693428837..5da5ffcd58bf19 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import Boom from 'boom';
import Hapi from 'hapi';
import { isFunction } from 'lodash/fp';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
@@ -14,6 +13,7 @@ import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_r
import { exportRulesSchema, exportRulesQuerySchema } from '../schemas/export_rules_schema';
import { getExportByObjectIds } from '../../rules/get_export_by_object_ids';
import { getExportAll } from '../../rules/get_export_all';
+import { transformError } from '../utils';
export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => {
return {
@@ -39,11 +39,21 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
try {
const exportSizeLimit = server.config().get('savedObjects.maxImportExportSize');
if (request.payload?.objects != null && request.payload.objects.length > exportSizeLimit) {
- return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`);
+ return headers
+ .response({
+ message: `Can't export more than ${exportSizeLimit} rules`,
+ status_code: 400,
+ })
+ .code(400);
} else {
const nonPackagedRulesCount = await getNonPackagedRulesCount({ alertsClient });
if (nonPackagedRulesCount > exportSizeLimit) {
- return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`);
+ return headers
+ .response({
+ message: `Can't export more than ${exportSizeLimit} rules`,
+ status_code: 400,
+ })
+ .code(400);
}
}
@@ -59,8 +69,14 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
return response
.header('Content-Disposition', `attachment; filename="${request.query.file_name}"`)
.header('Content-Type', 'application/ndjson');
- } catch {
- return Boom.badRequest(`Sorry, something went wrong to export rules`);
+ } catch (err) {
+ const error = transformError(err);
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
}
},
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts
index 5b12703590407c..b15c1db7222cf4 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts
@@ -11,7 +11,7 @@ import { findRules } from '../../rules/find_rules';
import { FindRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types';
import { findRulesSchema } from '../schemas/find_rules_schema';
import { ServerFacade } from '../../../../types';
-import { transformFindAlertsOrError } from './utils';
+import { transformFindAlerts } from './utils';
import { transformError } from '../utils';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
@@ -62,9 +62,25 @@ export const createFindRulesRoute = (): Hapi.ServerRoute => {
return results;
})
);
- return transformFindAlertsOrError(rules, ruleStatuses);
+ const transformed = transformFindAlerts(rules, ruleStatuses);
+ if (transformed == null) {
+ return headers
+ .response({
+ message: 'unknown data type, error transforming alert',
+ status_code: 500,
+ })
+ .code(500);
+ } else {
+ return transformed;
+ }
} catch (err) {
- return transformError(err);
+ const error = transformError(err);
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
}
},
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rule_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts
similarity index 100%
rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rule_status_route.test.ts
rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts
index ab6ee8e97a70f7..c999292ba7674d 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts
@@ -55,7 +55,13 @@ export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => {
rules_not_updated: rulesToUpdate.length,
};
} catch (err) {
- return transformError(err);
+ const error = transformError(err);
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
}
},
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts
index 0d57f5739fc15a..5e87c99d815efc 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import Boom from 'boom';
import Hapi from 'hapi';
import { chunk, isEmpty, isFunction } from 'lodash/fp';
import { extname } from 'path';
@@ -24,18 +23,12 @@ import {
} from '../utils';
import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson';
import { ImportRuleAlertRest } from '../../types';
-import { updateRules } from '../../rules/update_rules';
+import { patchRules } from '../../rules/patch_rules';
import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema';
import { KibanaRequest } from '../../../../../../../../../src/core/server';
type PromiseFromStreams = ImportRuleAlertRest | Error;
-/*
- * We were getting some error like that possible EventEmitter memory leak detected
- * So we decide to batch the update by 10 to avoid any complication in the node side
- * https://nodejs.org/docs/latest/api/events.html#events_emitter_setmaxlisteners_n
- *
- */
const CHUNK_PARSED_OBJECT_SIZE = 10;
export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => {
@@ -71,13 +64,17 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
const { filename } = request.payload.file.hapi;
const fileExtension = extname(filename).toLowerCase();
if (fileExtension !== '.ndjson') {
- return Boom.badRequest(`Invalid file extension ${fileExtension}`);
+ return headers
+ .response({
+ message: `Invalid file extension ${fileExtension}`,
+ status_code: 400,
+ })
+ .code(400);
}
const objectLimit = server.config().get('savedObjects.maxImportExportSize');
const readStream = createRulesStreamFromNdJson(request.payload.file, objectLimit);
const parsedObjects = await createPromiseFromStreams([readStream]);
-
const uniqueParsedObjects = Array.from(
parsedObjects
.reduce(
@@ -122,6 +119,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
}
const {
description,
+ enabled,
false_positives: falsePositives,
from,
immutable,
@@ -166,7 +164,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
alertsClient,
actionsClient,
description,
- enabled: false,
+ enabled,
falsePositives,
from,
immutable,
@@ -194,12 +192,12 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
});
resolve({ rule_id: ruleId, status_code: 200 });
} else if (rule != null && request.query.overwrite) {
- await updateRules({
+ await patchRules({
alertsClient,
actionsClient,
savedObjectsClient,
description,
- enabled: false,
+ enabled,
falsePositives,
from,
immutable,
@@ -232,7 +230,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
createBulkErrorObject({
ruleId,
statusCode: 409,
- message: `This Rule "${rule.name}" already exists`,
+ message: `rule_id: "${ruleId}" already exists`,
})
);
}
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts
new file mode 100644
index 00000000000000..aa0dd04786a2ee
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts
@@ -0,0 +1,160 @@
+/*
+ * 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 {
+ createMockServer,
+ createMockServerWithoutAlertClientDecoration,
+} from '../__mocks__/_mock_server';
+
+import { patchRulesRoute } from './patch_rules_route';
+import { ServerInjectOptions } from 'hapi';
+
+import {
+ getFindResult,
+ getResult,
+ updateActionResult,
+ typicalPayload,
+ getFindResultWithSingleHit,
+ getPatchBulkRequest,
+} from '../__mocks__/request_responses';
+import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
+import { patchRulesBulkRoute } from './patch_rules_bulk_route';
+import { BulkError } from '../utils';
+
+describe('patch_rules_bulk', () => {
+ let { server, alertsClient, actionsClient } = createMockServer();
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ ({ server, alertsClient, actionsClient } = createMockServer());
+ patchRulesBulkRoute(server);
+ });
+
+ describe('status codes with actionClient and alertClient', () => {
+ test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => {
+ alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
+ alertsClient.get.mockResolvedValue(getResult());
+ actionsClient.update.mockResolvedValue(updateActionResult());
+ alertsClient.update.mockResolvedValue(getResult());
+ const { statusCode } = await server.inject(getPatchBulkRequest());
+ expect(statusCode).toBe(200);
+ });
+
+ test('returns 200 as a response when updating a single rule that does not exist', async () => {
+ alertsClient.find.mockResolvedValue(getFindResult());
+ alertsClient.get.mockResolvedValue(getResult());
+ actionsClient.update.mockResolvedValue(updateActionResult());
+ alertsClient.update.mockResolvedValue(getResult());
+ const { statusCode } = await server.inject(getPatchBulkRequest());
+ expect(statusCode).toBe(200);
+ });
+
+ test('returns 404 within the payload when updating a single rule that does not exist', async () => {
+ alertsClient.find.mockResolvedValue(getFindResult());
+ alertsClient.get.mockResolvedValue(getResult());
+ actionsClient.update.mockResolvedValue(updateActionResult());
+ alertsClient.update.mockResolvedValue(getResult());
+ const { payload } = await server.inject(getPatchBulkRequest());
+ const parsed: BulkError[] = JSON.parse(payload);
+ const expected: BulkError[] = [
+ {
+ error: { message: 'rule_id: "rule-1" not found', status_code: 404 },
+ rule_id: 'rule-1',
+ },
+ ];
+ expect(parsed).toEqual(expected);
+ });
+
+ test('returns 404 if alertClient is not available on the route', async () => {
+ const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration();
+ patchRulesRoute(serverWithoutAlertClient);
+ const { statusCode } = await serverWithoutAlertClient.inject(getPatchBulkRequest());
+ expect(statusCode).toBe(404);
+ });
+ });
+
+ describe('validation', () => {
+ test('returns 400 if id is not given in either the body or the url', async () => {
+ alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
+ alertsClient.get.mockResolvedValue(getResult());
+ const { rule_id, ...noId } = typicalPayload();
+ const request: ServerInjectOptions = {
+ method: 'PATCH',
+ url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
+ payload: [noId],
+ };
+ const { statusCode } = await server.inject(request);
+ expect(statusCode).toBe(400);
+ });
+
+ test('returns errors as 200 to just indicate ok something happened', async () => {
+ alertsClient.find.mockResolvedValue(getFindResult());
+ actionsClient.update.mockResolvedValue(updateActionResult());
+ alertsClient.update.mockResolvedValue(getResult());
+ const request: ServerInjectOptions = {
+ method: 'PATCH',
+ url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
+ payload: [typicalPayload()],
+ };
+ const { statusCode } = await server.inject(request);
+ expect(statusCode).toEqual(200);
+ });
+
+ test('returns 404 in the payload if the record does not exist yet', async () => {
+ alertsClient.find.mockResolvedValue(getFindResult());
+ actionsClient.update.mockResolvedValue(updateActionResult());
+ alertsClient.update.mockResolvedValue(getResult());
+ const request: ServerInjectOptions = {
+ method: 'PATCH',
+ url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
+ payload: [typicalPayload()],
+ };
+ const { payload } = await server.inject(request);
+ const parsed: BulkError[] = JSON.parse(payload);
+ const expected: BulkError[] = [
+ {
+ error: { message: 'rule_id: "rule-1" not found', status_code: 404 },
+ rule_id: 'rule-1',
+ },
+ ];
+ expect(parsed).toEqual(expected);
+ });
+
+ test('returns 200 if type is query', async () => {
+ alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
+ alertsClient.get.mockResolvedValue(getResult());
+ actionsClient.update.mockResolvedValue(updateActionResult());
+ alertsClient.update.mockResolvedValue(getResult());
+ const request: ServerInjectOptions = {
+ method: 'PATCH',
+ url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
+ payload: [typicalPayload()],
+ };
+ const { statusCode } = await server.inject(request);
+ expect(statusCode).toBe(200);
+ });
+
+ test('returns 400 if type is not filter or kql', async () => {
+ alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
+ alertsClient.get.mockResolvedValue(getResult());
+ actionsClient.update.mockResolvedValue(updateActionResult());
+ alertsClient.update.mockResolvedValue(getResult());
+ const { type, ...noType } = typicalPayload();
+ const request: ServerInjectOptions = {
+ method: 'PATCH',
+ url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
+ payload: [
+ {
+ ...noType,
+ type: 'something-made-up',
+ },
+ ],
+ };
+ const { statusCode } = await server.inject(request);
+ expect(statusCode).toBe(400);
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts
new file mode 100644
index 00000000000000..00184b6c16b7e3
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts
@@ -0,0 +1,137 @@
+/*
+ * 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 Hapi from 'hapi';
+import { isFunction } from 'lodash/fp';
+import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
+import {
+ BulkPatchRulesRequest,
+ IRuleSavedAttributesSavedObjectAttributes,
+} from '../../rules/types';
+import { ServerFacade } from '../../../../types';
+import { transformOrBulkError, getIdBulkError } from './utils';
+import { transformBulkError } from '../utils';
+import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema';
+import { patchRules } from '../../rules/patch_rules';
+import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
+import { KibanaRequest } from '../../../../../../../../../src/core/server';
+
+export const createPatchRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => {
+ return {
+ method: 'PATCH',
+ path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
+ options: {
+ tags: ['access:siem'],
+ validate: {
+ options: {
+ abortEarly: false,
+ },
+ payload: patchRulesBulkSchema,
+ },
+ },
+ async handler(request: BulkPatchRulesRequest, headers) {
+ const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null;
+ const actionsClient = await server.plugins.actions.getActionsClientWithRequest(
+ KibanaRequest.from((request as unknown) as Hapi.Request)
+ );
+ const savedObjectsClient = isFunction(request.getSavedObjectsClient)
+ ? request.getSavedObjectsClient()
+ : null;
+ if (!alertsClient || !savedObjectsClient) {
+ return headers.response().code(404);
+ }
+
+ const rules = await Promise.all(
+ request.payload.map(async payloadRule => {
+ const {
+ description,
+ enabled,
+ false_positives: falsePositives,
+ from,
+ query,
+ language,
+ output_index: outputIndex,
+ saved_id: savedId,
+ timeline_id: timelineId,
+ timeline_title: timelineTitle,
+ meta,
+ filters,
+ rule_id: ruleId,
+ id,
+ index,
+ interval,
+ max_signals: maxSignals,
+ risk_score: riskScore,
+ name,
+ severity,
+ tags,
+ to,
+ type,
+ threat,
+ references,
+ version,
+ } = payloadRule;
+ const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)';
+ try {
+ const rule = await patchRules({
+ alertsClient,
+ actionsClient,
+ description,
+ enabled,
+ falsePositives,
+ from,
+ query,
+ language,
+ outputIndex,
+ savedId,
+ savedObjectsClient,
+ timelineId,
+ timelineTitle,
+ meta,
+ filters,
+ id,
+ ruleId,
+ index,
+ interval,
+ maxSignals,
+ riskScore,
+ name,
+ severity,
+ tags,
+ to,
+ type,
+ threat,
+ references,
+ version,
+ });
+ if (rule != null) {
+ const ruleStatuses = await savedObjectsClient.find<
+ IRuleSavedAttributesSavedObjectAttributes
+ >({
+ type: ruleStatusSavedObjectType,
+ perPage: 1,
+ sortField: 'statusDate',
+ sortOrder: 'desc',
+ search: rule.id,
+ searchFields: ['alertId'],
+ });
+ return transformOrBulkError(rule.id, rule, ruleStatuses.saved_objects[0]);
+ } else {
+ return getIdBulkError({ id, ruleId });
+ }
+ } catch (err) {
+ return transformBulkError(idOrRuleIdOrUnknown, err);
+ }
+ })
+ );
+ return rules;
+ },
+ };
+};
+
+export const patchRulesBulkRoute = (server: ServerFacade): void => {
+ server.route(createPatchRulesBulkRoute(server));
+};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts
new file mode 100644
index 00000000000000..d315d45046e2dd
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts
@@ -0,0 +1,129 @@
+/*
+ * 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 {
+ createMockServer,
+ createMockServerWithoutAlertClientDecoration,
+} from '../__mocks__/_mock_server';
+
+import { patchRulesRoute } from './patch_rules_route';
+import { ServerInjectOptions } from 'hapi';
+
+import {
+ getFindResult,
+ getFindResultStatus,
+ getResult,
+ updateActionResult,
+ getPatchRequest,
+ typicalPayload,
+ getFindResultWithSingleHit,
+} from '../__mocks__/request_responses';
+import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
+
+describe('patch_rules', () => {
+ let { server, alertsClient, actionsClient, savedObjectsClient } = createMockServer();
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ ({ server, alertsClient, actionsClient, savedObjectsClient } = createMockServer());
+ patchRulesRoute(server);
+ });
+
+ describe('status codes with actionClient and alertClient', () => {
+ test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => {
+ alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
+ alertsClient.get.mockResolvedValue(getResult());
+ actionsClient.update.mockResolvedValue(updateActionResult());
+ alertsClient.update.mockResolvedValue(getResult());
+ savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
+ const { statusCode } = await server.inject(getPatchRequest());
+ expect(statusCode).toBe(200);
+ });
+
+ test('returns 404 when updating a single rule that does not exist', async () => {
+ alertsClient.find.mockResolvedValue(getFindResult());
+ alertsClient.get.mockResolvedValue(getResult());
+ actionsClient.update.mockResolvedValue(updateActionResult());
+ alertsClient.update.mockResolvedValue(getResult());
+ savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
+ const { statusCode } = await server.inject(getPatchRequest());
+ expect(statusCode).toBe(404);
+ });
+
+ test('returns 404 if alertClient is not available on the route', async () => {
+ const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration();
+ patchRulesRoute(serverWithoutAlertClient);
+ const { statusCode } = await serverWithoutAlertClient.inject(getPatchRequest());
+ expect(statusCode).toBe(404);
+ });
+ });
+
+ describe('validation', () => {
+ test('returns 400 if id is not given in either the body or the url', async () => {
+ alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
+ alertsClient.get.mockResolvedValue(getResult());
+ savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
+ const { rule_id, ...noId } = typicalPayload();
+ const request: ServerInjectOptions = {
+ method: 'PATCH',
+ url: DETECTION_ENGINE_RULES_URL,
+ payload: {
+ payload: noId,
+ },
+ };
+ const { statusCode } = await server.inject(request);
+ expect(statusCode).toBe(400);
+ });
+
+ test('returns 404 if the record does not exist yet', async () => {
+ alertsClient.find.mockResolvedValue(getFindResult());
+ actionsClient.update.mockResolvedValue(updateActionResult());
+ alertsClient.update.mockResolvedValue(getResult());
+ savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
+ const request: ServerInjectOptions = {
+ method: 'PATCH',
+ url: DETECTION_ENGINE_RULES_URL,
+ payload: typicalPayload(),
+ };
+ const { statusCode } = await server.inject(request);
+ expect(statusCode).toBe(404);
+ });
+
+ test('returns 200 if type is query', async () => {
+ alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
+ alertsClient.get.mockResolvedValue(getResult());
+ actionsClient.update.mockResolvedValue(updateActionResult());
+ alertsClient.update.mockResolvedValue(getResult());
+ savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
+ const request: ServerInjectOptions = {
+ method: 'PATCH',
+ url: DETECTION_ENGINE_RULES_URL,
+ payload: typicalPayload(),
+ };
+ const { statusCode } = await server.inject(request);
+ expect(statusCode).toBe(200);
+ });
+
+ test('returns 400 if type is not filter or kql', async () => {
+ alertsClient.find.mockResolvedValue(getFindResultWithSingleHit());
+ alertsClient.get.mockResolvedValue(getResult());
+ actionsClient.update.mockResolvedValue(updateActionResult());
+ alertsClient.update.mockResolvedValue(getResult());
+ savedObjectsClient.find.mockResolvedValue(getFindResultStatus());
+ const { type, ...noType } = typicalPayload();
+ const request: ServerInjectOptions = {
+ method: 'PATCH',
+ url: DETECTION_ENGINE_RULES_URL,
+ payload: {
+ ...noType,
+ type: 'something-made-up',
+ },
+ };
+ const { statusCode } = await server.inject(request);
+ expect(statusCode).toBe(400);
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts
new file mode 100644
index 00000000000000..e27ae81362f27e
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts
@@ -0,0 +1,151 @@
+/*
+ * 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 Hapi from 'hapi';
+import { isFunction } from 'lodash/fp';
+import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
+import { patchRules } from '../../rules/patch_rules';
+import { PatchRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types';
+import { patchRulesSchema } from '../schemas/patch_rules_schema';
+import { ServerFacade } from '../../../../types';
+import { getIdError, transform } from './utils';
+import { transformError } from '../utils';
+import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
+import { KibanaRequest } from '../../../../../../../../../src/core/server';
+
+export const createPatchRulesRoute = (server: ServerFacade): Hapi.ServerRoute => {
+ return {
+ method: 'PATCH',
+ path: DETECTION_ENGINE_RULES_URL,
+ options: {
+ tags: ['access:siem'],
+ validate: {
+ options: {
+ abortEarly: false,
+ },
+ payload: patchRulesSchema,
+ },
+ },
+ async handler(request: PatchRulesRequest, headers) {
+ const {
+ description,
+ enabled,
+ false_positives: falsePositives,
+ from,
+ query,
+ language,
+ output_index: outputIndex,
+ saved_id: savedId,
+ timeline_id: timelineId,
+ timeline_title: timelineTitle,
+ meta,
+ filters,
+ rule_id: ruleId,
+ id,
+ index,
+ interval,
+ max_signals: maxSignals,
+ risk_score: riskScore,
+ name,
+ severity,
+ tags,
+ to,
+ type,
+ threat,
+ references,
+ version,
+ } = request.payload;
+
+ const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null;
+ const actionsClient = await server.plugins.actions.getActionsClientWithRequest(
+ KibanaRequest.from((request as unknown) as Hapi.Request)
+ );
+ const savedObjectsClient = isFunction(request.getSavedObjectsClient)
+ ? request.getSavedObjectsClient()
+ : null;
+ if (!alertsClient || !savedObjectsClient) {
+ return headers.response().code(404);
+ }
+
+ try {
+ const rule = await patchRules({
+ alertsClient,
+ actionsClient,
+ description,
+ enabled,
+ falsePositives,
+ from,
+ query,
+ language,
+ outputIndex,
+ savedId,
+ savedObjectsClient,
+ timelineId,
+ timelineTitle,
+ meta,
+ filters,
+ id,
+ ruleId,
+ index,
+ interval,
+ maxSignals,
+ riskScore,
+ name,
+ severity,
+ tags,
+ to,
+ type,
+ threat,
+ references,
+ version,
+ });
+ if (rule != null) {
+ const ruleStatuses = await savedObjectsClient.find<
+ IRuleSavedAttributesSavedObjectAttributes
+ >({
+ type: ruleStatusSavedObjectType,
+ perPage: 1,
+ sortField: 'statusDate',
+ sortOrder: 'desc',
+ search: rule.id,
+ searchFields: ['alertId'],
+ });
+ const transformed = transform(rule, ruleStatuses.saved_objects[0]);
+ if (transformed == null) {
+ return headers
+ .response({
+ message: 'Internal error transforming rules',
+ status_code: 500,
+ })
+ .code(500);
+ } else {
+ return transformed;
+ }
+ } else {
+ const error = getIdError({ id, ruleId });
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
+ }
+ } catch (err) {
+ const error = transformError(err);
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
+ }
+ },
+ };
+};
+
+export const patchRulesRoute = (server: ServerFacade) => {
+ server.route(createPatchRulesRoute(server));
+};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts
index 55fecdc14f7558..e82ad92704695c 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts
@@ -7,7 +7,7 @@
import Hapi from 'hapi';
import { isFunction } from 'lodash/fp';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
-import { getIdError, transformOrError } from './utils';
+import { getIdError, transform } from './utils';
import { transformError } from '../utils';
import { readRules } from '../../rules/read_rules';
@@ -54,12 +54,34 @@ export const createReadRulesRoute: Hapi.ServerRoute = {
search: rule.id,
searchFields: ['alertId'],
});
- return transformOrError(rule, ruleStatuses.saved_objects[0]);
+ const transformedOrError = transform(rule, ruleStatuses.saved_objects[0]);
+ if (transformedOrError == null) {
+ return headers
+ .response({
+ message: 'Internal error transforming rules',
+ status_code: 500,
+ })
+ .code(500);
+ } else {
+ return transformedOrError;
+ }
} else {
- return getIdError({ id, ruleId });
+ const error = getIdError({ id, ruleId });
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
}
} catch (err) {
- return transformError(err);
+ const error = transformError(err);
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
}
},
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts
index 8c7558d6d4fb57..671497f9f65db9 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts
@@ -13,11 +13,11 @@ import {
} from '../../rules/types';
import { ServerFacade } from '../../../../types';
import { transformOrBulkError, getIdBulkError } from './utils';
-import { transformBulkError } from '../utils';
+import { transformBulkError, getIndex } from '../utils';
import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema';
-import { updateRules } from '../../rules/update_rules';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
import { KibanaRequest } from '../../../../../../../../../src/core/server';
+import { updateRules } from '../../rules/update_rules';
export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => {
return {
@@ -44,7 +44,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
return headers.response().code(404);
}
- const rules = Promise.all(
+ const rules = await Promise.all(
request.payload.map(async payloadRule => {
const {
description,
@@ -74,6 +74,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
references,
version,
} = payloadRule;
+ const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server);
const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)';
try {
const rule = await updateRules({
@@ -81,11 +82,12 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou
actionsClient,
description,
enabled,
+ immutable: false,
falsePositives,
from,
query,
language,
- outputIndex,
+ outputIndex: finalIndex,
savedId,
savedObjectsClient,
timelineId,
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts
index 590307e06a26a9..a01627d2094b79 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts
@@ -7,14 +7,14 @@
import Hapi from 'hapi';
import { isFunction } from 'lodash/fp';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
-import { updateRules } from '../../rules/update_rules';
import { UpdateRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types';
import { updateRulesSchema } from '../schemas/update_rules_schema';
import { ServerFacade } from '../../../../types';
-import { getIdError, transformOrError } from './utils';
-import { transformError } from '../utils';
+import { getIdError, transform } from './utils';
+import { transformError, getIndex } from '../utils';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
import { KibanaRequest } from '../../../../../../../../../src/core/server';
+import { updateRules } from '../../rules/update_rules';
export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute => {
return {
@@ -39,8 +39,8 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
language,
output_index: outputIndex,
saved_id: savedId,
- timeline_id: timelineId = null,
- timeline_title: timelineTitle = null,
+ timeline_id: timelineId,
+ timeline_title: timelineTitle,
meta,
filters,
rule_id: ruleId,
@@ -71,6 +71,7 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
}
try {
+ const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server);
const rule = await updateRules({
alertsClient,
actionsClient,
@@ -78,9 +79,10 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
enabled,
falsePositives,
from,
+ immutable: false,
query,
language,
- outputIndex,
+ outputIndex: finalIndex,
savedId,
savedObjectsClient,
timelineId,
@@ -113,12 +115,34 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
search: rule.id,
searchFields: ['alertId'],
});
- return transformOrError(rule, ruleStatuses.saved_objects[0]);
+ const transformed = transform(rule, ruleStatuses.saved_objects[0]);
+ if (transformed == null) {
+ return headers
+ .response({
+ message: 'Internal error transforming rules',
+ status_code: 500,
+ })
+ .code(500);
+ } else {
+ return transformed;
+ }
} else {
- return getIdError({ id, ruleId });
+ const error = getIdError({ id, ruleId });
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
}
} catch (err) {
- return transformError(err);
+ const error = transformError(err);
+ return headers
+ .response({
+ message: error.message,
+ status_code: error.statusCode,
+ })
+ .code(error.statusCode);
}
},
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts
index ec11a8fb2da39d..7e7d67333e78d9 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts
@@ -4,13 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import Boom from 'boom';
-
import {
transformAlertToRule,
getIdError,
- transformFindAlertsOrError,
- transformOrError,
+ transformFindAlerts,
+ transform,
transformTags,
getIdBulkError,
transformOrBulkError,
@@ -547,55 +545,87 @@ describe('utils', () => {
});
describe('getIdError', () => {
+ test('it should have a status code', () => {
+ const error = getIdError({ id: '123', ruleId: undefined });
+ expect(error).toEqual({
+ message: 'id: "123" not found',
+ statusCode: 404,
+ });
+ });
+
test('outputs message about id not being found if only id is defined and ruleId is undefined', () => {
- const boom = getIdError({ id: '123', ruleId: undefined });
- expect(boom.message).toEqual('id: "123" not found');
+ const error = getIdError({ id: '123', ruleId: undefined });
+ expect(error).toEqual({
+ message: 'id: "123" not found',
+ statusCode: 404,
+ });
});
test('outputs message about id not being found if only id is defined and ruleId is null', () => {
- const boom = getIdError({ id: '123', ruleId: null });
- expect(boom.message).toEqual('id: "123" not found');
+ const error = getIdError({ id: '123', ruleId: null });
+ expect(error).toEqual({
+ message: 'id: "123" not found',
+ statusCode: 404,
+ });
});
test('outputs message about ruleId not being found if only ruleId is defined and id is undefined', () => {
- const boom = getIdError({ id: undefined, ruleId: 'rule-id-123' });
- expect(boom.message).toEqual('rule_id: "rule-id-123" not found');
+ const error = getIdError({ id: undefined, ruleId: 'rule-id-123' });
+ expect(error).toEqual({
+ message: 'rule_id: "rule-id-123" not found',
+ statusCode: 404,
+ });
});
test('outputs message about ruleId not being found if only ruleId is defined and id is null', () => {
- const boom = getIdError({ id: null, ruleId: 'rule-id-123' });
- expect(boom.message).toEqual('rule_id: "rule-id-123" not found');
+ const error = getIdError({ id: null, ruleId: 'rule-id-123' });
+ expect(error).toEqual({
+ message: 'rule_id: "rule-id-123" not found',
+ statusCode: 404,
+ });
});
test('outputs message about both being not defined when both are undefined', () => {
- const boom = getIdError({ id: undefined, ruleId: undefined });
- expect(boom.message).toEqual('id or rule_id should have been defined');
+ const error = getIdError({ id: undefined, ruleId: undefined });
+ expect(error).toEqual({
+ message: 'id or rule_id should have been defined',
+ statusCode: 404,
+ });
});
test('outputs message about both being not defined when both are null', () => {
- const boom = getIdError({ id: null, ruleId: null });
- expect(boom.message).toEqual('id or rule_id should have been defined');
+ const error = getIdError({ id: null, ruleId: null });
+ expect(error).toEqual({
+ message: 'id or rule_id should have been defined',
+ statusCode: 404,
+ });
});
test('outputs message about both being not defined when id is null and ruleId is undefined', () => {
- const boom = getIdError({ id: null, ruleId: undefined });
- expect(boom.message).toEqual('id or rule_id should have been defined');
+ const error = getIdError({ id: null, ruleId: undefined });
+ expect(error).toEqual({
+ message: 'id or rule_id should have been defined',
+ statusCode: 404,
+ });
});
test('outputs message about both being not defined when id is undefined and ruleId is null', () => {
- const boom = getIdError({ id: undefined, ruleId: null });
- expect(boom.message).toEqual('id or rule_id should have been defined');
+ const error = getIdError({ id: undefined, ruleId: null });
+ expect(error).toEqual({
+ message: 'id or rule_id should have been defined',
+ statusCode: 404,
+ });
});
});
- describe('transformFindAlertsOrError', () => {
+ describe('transformFindAlerts', () => {
test('outputs empty data set when data set is empty correct', () => {
- const output = transformFindAlertsOrError({ data: [] });
+ const output = transformFindAlerts({ data: [] });
expect(output).toEqual({ data: [] });
});
test('outputs 200 if the data is of type siem alert', () => {
- const output = transformFindAlertsOrError({
+ const output = transformFindAlerts({
data: [getResult()],
});
const expected: OutputRuleAlertRest = {
@@ -663,14 +693,14 @@ describe('utils', () => {
});
test('returns 500 if the data is not of type siem alert', () => {
- const output = transformFindAlertsOrError({ data: [{ random: 1 }] });
- expect((output as Boom).message).toEqual('Internal error transforming');
+ const output = transformFindAlerts({ data: [{ random: 1 }] });
+ expect(output).toBeNull();
});
});
describe('transformOrError', () => {
test('outputs 200 if the data is of type siem alert', () => {
- const output = transformOrError(getResult());
+ const output = transform(getResult());
const expected: OutputRuleAlertRest = {
created_by: 'elastic',
created_at: '2019-12-13T16:40:33.400Z',
@@ -734,8 +764,8 @@ describe('utils', () => {
});
test('returns 500 if the data is not of type siem alert', () => {
- const output = transformOrError({ data: [{ random: 1 }] });
- expect((output as Boom).message).toEqual('Internal error transforming');
+ const output = transform({ data: [{ random: 1 }] });
+ expect(output).toBeNull();
});
});
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts
index b45db53c13d883..abb94c10209dca 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import Boom from 'boom';
import { pickBy } from 'lodash/fp';
import { SavedObject } from 'kibana/server';
import { INTERNAL_IDENTIFIER } from '../../../../../common/constants';
@@ -24,6 +23,7 @@ import {
createSuccessObject,
ImportSuccessError,
createImportErrorObject,
+ OutputError,
} from '../utils';
export const getIdError = ({
@@ -32,13 +32,22 @@ export const getIdError = ({
}: {
id: string | undefined | null;
ruleId: string | undefined | null;
-}) => {
+}): OutputError => {
if (id != null) {
- return Boom.notFound(`id: "${id}" not found`);
+ return {
+ message: `id: "${id}" not found`,
+ statusCode: 404,
+ };
} else if (ruleId != null) {
- return Boom.notFound(`rule_id: "${ruleId}" not found`);
+ return {
+ message: `rule_id: "${ruleId}" not found`,
+ statusCode: 404,
+ };
} else {
- return Boom.notFound('id or rule_id should have been defined');
+ return {
+ message: 'id or rule_id should have been defined',
+ statusCode: 404,
+ };
}
};
@@ -136,10 +145,10 @@ export const transformAlertsToRules = (
return alerts.map(alert => transformAlertToRule(alert));
};
-export const transformFindAlertsOrError = (
+export const transformFindAlerts = (
findResults: { data: unknown[] },
ruleStatuses?: unknown[]
-): unknown | Boom => {
+): unknown | null => {
if (!ruleStatuses && isAlertTypes(findResults.data)) {
findResults.data = findResults.data.map(alert => transformAlertToRule(alert));
return findResults;
@@ -150,14 +159,14 @@ export const transformFindAlertsOrError = (
);
return findResults;
} else {
- return new Boom('Internal error transforming', { statusCode: 500 });
+ return null;
}
};
-export const transformOrError = (
+export const transform = (
alert: unknown,
ruleStatus?: unknown
-): Partial | Boom => {
+): Partial | null => {
if (!ruleStatus && isAlertType(alert)) {
return transformAlertToRule(alert);
}
@@ -166,7 +175,7 @@ export const transformOrError = (
} else if (isAlertType(alert) && isRuleStatusSavedObjectType(ruleStatus)) {
return transformAlertToRule(alert, ruleStatus);
} else {
- return new Boom('Internal error transforming', { statusCode: 500 });
+ return null;
}
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts
index 1eab50848b822a..2a64478962ced4 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts
@@ -5,7 +5,7 @@
*/
import { createRulesBulkSchema } from './create_rules_bulk_schema';
-import { UpdateRuleAlertParamsRest } from '../../rules/types';
+import { PatchRuleAlertParamsRest } from '../../rules/types';
// only the basics of testing are here.
// see: create_rules_schema.test.ts for the bulk of the validation tests
@@ -13,7 +13,7 @@ import { UpdateRuleAlertParamsRest } from '../../rules/types';
describe('create_rules_bulk_schema', () => {
test('can take an empty array and validate it', () => {
expect(
- createRulesBulkSchema.validate>>([]).error
+ createRulesBulkSchema.validate>>([]).error
).toBeFalsy();
});
@@ -29,7 +29,7 @@ describe('create_rules_bulk_schema', () => {
test('single array of [id] does validate', () => {
expect(
- createRulesBulkSchema.validate>>([
+ createRulesBulkSchema.validate>>([
{
rule_id: 'rule-1',
risk_score: 50,
@@ -49,7 +49,7 @@ describe('create_rules_bulk_schema', () => {
test('two values of [id] does validate', () => {
expect(
- createRulesBulkSchema.validate>>([
+ createRulesBulkSchema.validate>>([
{
rule_id: 'rule-1',
risk_score: 50,
@@ -82,7 +82,7 @@ describe('create_rules_bulk_schema', () => {
test('The default for "from" will be "now-6m"', () => {
expect(
- createRulesBulkSchema.validate>([
+ createRulesBulkSchema.validate>([
{
rule_id: 'rule-1',
risk_score: 50,
@@ -102,7 +102,7 @@ describe('create_rules_bulk_schema', () => {
test('The default for "to" will be "now"', () => {
expect(
- createRulesBulkSchema.validate>([
+ createRulesBulkSchema.validate>([
{
rule_id: 'rule-1',
risk_score: 50,
@@ -122,7 +122,7 @@ describe('create_rules_bulk_schema', () => {
test('You cannot set the severity to a value other than low, medium, high, or critical', () => {
expect(
- createRulesBulkSchema.validate>([
+ createRulesBulkSchema.validate>([
{
rule_id: 'rule-1',
risk_score: 50,
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts
index d9605a265d28b8..052a149f3d4dc1 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts
@@ -5,12 +5,12 @@
*/
import { createRulesSchema } from './create_rules_schema';
-import { UpdateRuleAlertParamsRest } from '../../rules/types';
+import { PatchRuleAlertParamsRest } from '../../rules/types';
import { ThreatParams, RuleAlertParamsRest } from '../../types';
describe('create rules schema', () => {
test('empty objects do not validate', () => {
- expect(createRulesSchema.validate>({}).error).toBeTruthy();
+ expect(createRulesSchema.validate>({}).error).toBeTruthy();
});
test('made up values do not validate', () => {
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts
new file mode 100644
index 00000000000000..cbcb9eba75bc15
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 } from './patch_rules_bulk_schema';
+import { PatchRuleAlertParamsRest } from '../../rules/types';
+
+// 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', () => {
+ expect(
+ patchRulesBulkSchema.validate>>([]).error
+ ).toBeFalsy();
+ });
+
+ test('made up values do not validate', () => {
+ expect(
+ patchRulesBulkSchema.validate<[{ madeUp: string }]>([
+ {
+ madeUp: 'hi',
+ },
+ ]).error
+ ).toBeTruthy();
+ });
+
+ test('single array of [id] does validate', () => {
+ expect(
+ patchRulesBulkSchema.validate>>([
+ {
+ id: 'rule-1',
+ },
+ ]).error
+ ).toBeFalsy();
+ });
+
+ test('two values of [id] does validate', () => {
+ expect(
+ patchRulesBulkSchema.validate>>([
+ {
+ id: 'rule-1',
+ },
+ {
+ id: 'rule-2',
+ },
+ ]).error
+ ).toBeFalsy();
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts
new file mode 100644
index 00000000000000..ff813bce84add4
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.ts
@@ -0,0 +1,11 @@
+/*
+ * 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 Joi from 'joi';
+
+import { patchRulesSchema } from './patch_rules_schema';
+
+export const patchRulesBulkSchema = Joi.array().items(patchRulesSchema);
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts
new file mode 100644
index 00000000000000..11bed22e1c047c
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts
@@ -0,0 +1,1015 @@
+/*
+ * 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';
+import { PatchRuleAlertParamsRest } from '../../rules/types';
+import { ThreatParams } from '../../types';
+
+describe('patch rules schema', () => {
+ test('empty objects do not validate as they require at least id or rule_id', () => {
+ expect(patchRulesSchema.validate>({}).error).toBeTruthy();
+ });
+
+ test('made up values do not validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ madeUp: 'hi',
+ }).error
+ ).toBeTruthy();
+ });
+
+ test('[id] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[rule_id] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ rule_id: 'rule-1',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[id] and [rule_id] does not validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'id-1',
+ rule_id: 'rule-1',
+ }).error.message
+ ).toEqual('"value" contains a conflict between exclusive peers [id, rule_id]');
+ });
+
+ test('[rule_id, description] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ rule_id: 'rule-1',
+ description: 'some description',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[id, description] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[id, risk_score] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ risk_score: 10,
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[rule_id, description, from] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ rule_id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[id, description, from] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[rule_id, description, from, to] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ rule_id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[id, description, from, to] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[rule_id, description, from, to, name] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ rule_id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ name: 'some-name',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[id, description, from, to, name] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ name: 'some-name',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[rule_id, description, from, to, name, severity] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ rule_id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ name: 'some-name',
+ severity: 'low',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[id, description, from, to, name, severity] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ name: 'some-name',
+ severity: 'low',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[rule_id, description, from, to, name, severity, type] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ rule_id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ name: 'some-name',
+ severity: 'low',
+ type: 'query',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[id, description, from, to, name, severity, type] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ name: 'some-name',
+ severity: 'low',
+ type: 'query',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ rule_id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[id, description, from, to, name, severity, type, interval] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[rule_id, description, from, to, index, name, severity, interval, type] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ rule_id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[id, description, from, to, index, name, severity, interval, type] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ rule_id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ query: 'some query',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ query: 'some query',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ rule_id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ query: 'some query',
+ language: 'kuery',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ query: 'some query',
+ language: 'kuery',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[rule_id, description, from, to, index, name, severity, type, filter] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ rule_id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('[id, description, from, to, index, name, severity, type, filter] does validate', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('allows references to be sent as a valid value to patch with', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('does not default references to an array', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ query: 'some-query',
+ language: 'kuery',
+ }).value.references
+ ).toEqual(undefined);
+ });
+
+ test('does not default interval', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ type: 'query',
+ }).value.interval
+ ).toEqual(undefined);
+ });
+
+ test('does not default max signal', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ }).value.max_signals
+ ).toEqual(undefined);
+ });
+
+ test('references cannot be numbers', () => {
+ expect(
+ patchRulesSchema.validate<
+ Partial> & { references: number[] }
+ >({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ query: 'some-query',
+ language: 'kuery',
+ references: [5],
+ }).error.message
+ ).toEqual(
+ 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]'
+ );
+ });
+
+ test('indexes cannot be numbers', () => {
+ expect(
+ patchRulesSchema.validate<
+ Partial> & { index: number[] }
+ >({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: [5],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ query: 'some-query',
+ language: 'kuery',
+ }).error.message
+ ).toEqual(
+ 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]'
+ );
+ });
+
+ test('saved_id is not required when type is saved_query and will validate without it', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'saved_query',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('saved_id validates with saved_query', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'saved_query',
+ saved_id: 'some id',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('saved_query type can have filters with it', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'saved_query',
+ saved_id: 'some id',
+ filters: [],
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('language validates with kuery', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('language validates with lucene', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'lucene',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('language does not validate with something made up', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'something-made-up',
+ }).error.message
+ ).toEqual('child "language" fails because ["language" must be one of [kuery, lucene]]');
+ });
+
+ test('max_signals cannot be negative', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: -1,
+ }).error.message
+ ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]');
+ });
+
+ test('max_signals cannot be zero', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 0,
+ }).error.message
+ ).toEqual('child "max_signals" fails because ["max_signals" must be greater than 0]');
+ });
+
+ test('max_signals can be 1', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('meta can be patched', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ meta: { whateverYouWant: 'anything_at_all' },
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('You cannot patch meta as a string', () => {
+ expect(
+ patchRulesSchema.validate & { meta: string }>>(
+ {
+ id: 'rule-1',
+ meta: 'should not work',
+ }
+ ).error.message
+ ).toEqual('child "meta" fails because ["meta" must be an object]');
+ });
+
+ test('filters cannot be a string', () => {
+ expect(
+ patchRulesSchema.validate<
+ Partial & { filters: string }>
+ >({
+ rule_id: 'rule-1',
+ type: 'query',
+ filters: 'some string',
+ }).error.message
+ ).toEqual('child "filters" fails because ["filters" must be an array]');
+ });
+
+ test('threat is not defaulted to empty array on patch', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ }).value.threat
+ ).toBe(undefined);
+ });
+
+ test('threat is not defaulted to undefined on patch with empty array', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ threat: [],
+ }).value.threat
+ ).toMatchObject([]);
+ });
+
+ test('threat is valid when updated with all sub-objects', () => {
+ const expected: ThreatParams[] = [
+ {
+ framework: 'fake',
+ tactic: {
+ id: 'fakeId',
+ name: 'fakeName',
+ reference: 'fakeRef',
+ },
+ technique: [
+ {
+ id: 'techniqueId',
+ name: 'techniqueName',
+ reference: 'techniqueRef',
+ },
+ ],
+ },
+ ];
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ threat: [
+ {
+ framework: 'fake',
+ tactic: {
+ id: 'fakeId',
+ name: 'fakeName',
+ reference: 'fakeRef',
+ },
+ technique: [
+ {
+ id: 'techniqueId',
+ name: 'techniqueName',
+ reference: 'techniqueRef',
+ },
+ ],
+ },
+ ],
+ }).value.threat
+ ).toMatchObject(expected);
+ });
+
+ test('threat is invalid when updated with missing property framework', () => {
+ expect(
+ patchRulesSchema.validate<
+ Partial> & {
+ threat: Array>>;
+ }
+ >({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ threat: [
+ {
+ tactic: {
+ id: 'fakeId',
+ name: 'fakeName',
+ reference: 'fakeRef',
+ },
+ technique: [
+ {
+ id: 'techniqueId',
+ name: 'techniqueName',
+ reference: 'techniqueRef',
+ },
+ ],
+ },
+ ],
+ }).error.message
+ ).toEqual(
+ 'child "threat" fails because ["threat" at position 0 fails because [child "framework" fails because ["framework" is required]]]'
+ );
+ });
+
+ test('threat is invalid when updated with missing tactic sub-object', () => {
+ expect(
+ patchRulesSchema.validate<
+ Partial> & {
+ threat: Array>>;
+ }
+ >({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ threat: [
+ {
+ framework: 'fake',
+ technique: [
+ {
+ id: 'techniqueId',
+ name: 'techniqueName',
+ reference: 'techniqueRef',
+ },
+ ],
+ },
+ ],
+ }).error.message
+ ).toEqual(
+ 'child "threat" fails because ["threat" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]'
+ );
+ });
+
+ test('threat is invalid when updated with missing technique', () => {
+ expect(
+ patchRulesSchema.validate<
+ Partial> & {
+ threat: Array>>;
+ }
+ >({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ threat: [
+ {
+ framework: 'fake',
+ tactic: {
+ id: 'techniqueId',
+ name: 'techniqueName',
+ reference: 'techniqueRef',
+ },
+ },
+ ],
+ }).error.message
+ ).toEqual(
+ 'child "threat" fails because ["threat" at position 0 fails because [child "technique" fails because ["technique" is required]]]'
+ );
+ });
+
+ test('validates with timeline_id and timeline_title', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'saved_query',
+ saved_id: 'some id',
+ timeline_id: 'some-id',
+ timeline_title: 'some-title',
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('You cannot omit timeline_title when timeline_id is present', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'saved_query',
+ saved_id: 'some id',
+ timeline_id: 'some-id',
+ }).error.message
+ ).toEqual('child "timeline_title" fails because ["timeline_title" is required]');
+ });
+
+ test('You cannot have a null value for timeline_title when timeline_id is present', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'saved_query',
+ saved_id: 'some id',
+ timeline_id: 'timeline-id',
+ timeline_title: null,
+ }).error.message
+ ).toEqual('child "timeline_title" fails because ["timeline_title" must be a string]');
+ });
+
+ test('You cannot have empty string for timeline_title when timeline_id is present', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'saved_query',
+ saved_id: 'some id',
+ timeline_id: 'some-id',
+ timeline_title: '',
+ }).error.message
+ ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed to be empty]');
+ });
+
+ test('You cannot have timeline_title with an empty timeline_id', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'saved_query',
+ saved_id: 'some id',
+ timeline_id: '',
+ timeline_title: 'some-title',
+ }).error.message
+ ).toEqual('child "timeline_id" fails because ["timeline_id" is not allowed to be empty]');
+ });
+
+ test('You cannot have timeline_title without timeline_id', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'saved_query',
+ saved_id: 'some id',
+ timeline_title: 'some-title',
+ }).error.message
+ ).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]');
+ });
+
+ test('You cannot set the severity to a value other than low, medium, high, or critical', () => {
+ expect(
+ patchRulesSchema.validate>({
+ id: 'rule-1',
+ risk_score: 50,
+ description: 'some description',
+ name: 'some-name',
+ severity: 'junk',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ version: 1,
+ }).error.message
+ ).toEqual(
+ 'child "severity" fails because ["severity" must be one of [low, medium, high, critical]]'
+ );
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts
new file mode 100644
index 00000000000000..d0ed1af01833b3
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 Joi from 'joi';
+
+/* eslint-disable @typescript-eslint/camelcase */
+import {
+ enabled,
+ description,
+ false_positives,
+ filters,
+ from,
+ index,
+ rule_id,
+ interval,
+ query,
+ language,
+ output_index,
+ saved_id,
+ timeline_id,
+ timeline_title,
+ meta,
+ risk_score,
+ max_signals,
+ name,
+ severity,
+ tags,
+ to,
+ type,
+ threat,
+ references,
+ id,
+ version,
+} from './schemas';
+/* eslint-enable @typescript-eslint/camelcase */
+
+export const patchRulesSchema = Joi.object({
+ description,
+ enabled,
+ false_positives,
+ filters,
+ from,
+ rule_id,
+ id,
+ index,
+ interval,
+ query: query.allow(''),
+ language,
+ output_index,
+ saved_id,
+ timeline_id,
+ timeline_title,
+ meta,
+ risk_score,
+ max_signals,
+ name,
+ severity,
+ tags,
+ to,
+ type,
+ threat,
+ references,
+ version,
+}).xor('id', 'rule_id');
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts
index ab1ffaab491651..7ea7fcbd1d86b3 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts
@@ -5,7 +5,7 @@
*/
import { queryRulesBulkSchema } from './query_rules_bulk_schema';
-import { UpdateRuleAlertParamsRest } from '../../rules/types';
+import { PatchRuleAlertParamsRest } from '../../rules/types';
// only the basics of testing are here.
// see: query_rules_bulk_schema.test.ts for the bulk of the validation tests
@@ -13,13 +13,13 @@ import { UpdateRuleAlertParamsRest } from '../../rules/types';
describe('query_rules_bulk_schema', () => {
test('can take an empty array and validate it', () => {
expect(
- queryRulesBulkSchema.validate>>([]).error
+ queryRulesBulkSchema.validate>>([]).error
).toBeFalsy();
});
test('both rule_id and id being supplied do not validate', () => {
expect(
- queryRulesBulkSchema.validate>>([
+ queryRulesBulkSchema.validate>>([
{
rule_id: '1',
id: '1',
@@ -32,7 +32,7 @@ describe('query_rules_bulk_schema', () => {
test('both rule_id and id being supplied do not validate if one array element works but the second does not', () => {
expect(
- queryRulesBulkSchema.validate>>([
+ queryRulesBulkSchema.validate>>([
{
id: '1',
},
@@ -48,13 +48,13 @@ describe('query_rules_bulk_schema', () => {
test('only id validates', () => {
expect(
- queryRulesBulkSchema.validate>>([{ id: '1' }]).error
+ queryRulesBulkSchema.validate>>([{ id: '1' }]).error
).toBeFalsy();
});
test('only id validates with two elements', () => {
expect(
- queryRulesBulkSchema.validate>>([
+ queryRulesBulkSchema.validate>>([
{ id: '1' },
{ id: '2' },
]).error
@@ -63,14 +63,14 @@ describe('query_rules_bulk_schema', () => {
test('only rule_id validates', () => {
expect(
- queryRulesBulkSchema.validate>>([{ rule_id: '1' }])
+ queryRulesBulkSchema.validate>>([{ rule_id: '1' }])
.error
).toBeFalsy();
});
test('only rule_id validates with two elements', () => {
expect(
- queryRulesBulkSchema.validate>>([
+ queryRulesBulkSchema.validate>>([
{ rule_id: '1' },
{ rule_id: '2' },
]).error
@@ -79,7 +79,7 @@ describe('query_rules_bulk_schema', () => {
test('both id and rule_id validates with two separate elements', () => {
expect(
- queryRulesBulkSchema.validate>>([
+ queryRulesBulkSchema.validate>>([
{ id: '1' },
{ rule_id: '2' },
]).error
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts
index c89d60e773a779..0f392e399f36c5 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts
@@ -5,29 +5,29 @@
*/
import { queryRulesSchema } from './query_rules_schema';
-import { UpdateRuleAlertParamsRest } from '../../rules/types';
+import { PatchRuleAlertParamsRest } from '../../rules/types';
describe('queryRulesSchema', () => {
test('empty objects do not validate', () => {
- expect(queryRulesSchema.validate>({}).error).toBeTruthy();
+ expect(queryRulesSchema.validate>({}).error).toBeTruthy();
});
test('both rule_id and id being supplied do not validate', () => {
expect(
- queryRulesSchema.validate>({ rule_id: '1', id: '1' }).error
+ queryRulesSchema.validate>({ rule_id: '1', id: '1' }).error
.message
).toEqual('"value" contains a conflict between exclusive peers [id, rule_id]');
});
test('only id validates', () => {
expect(
- queryRulesSchema.validate>({ id: '1' }).error
+ queryRulesSchema.validate>({ id: '1' }).error
).toBeFalsy();
});
test('only rule_id validates', () => {
expect(
- queryRulesSchema.validate>({ rule_id: '1' }).error
+ queryRulesSchema.validate>({ rule_id: '1' }).error
).toBeFalsy();
});
});
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts
index 2b1bad39eb6861..e866260662ad7b 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts
@@ -31,7 +31,17 @@ describe('update_rules_bulk_schema', () => {
expect(
updateRulesBulkSchema.validate>>([
{
- id: 'rule-1',
+ id: 'id-1',
+ risk_score: 50,
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ name: 'some-name',
+ severity: 'low',
+ type: 'query',
+ query: 'some query',
+ index: ['index-1'],
+ interval: '5m',
},
]).error
).toBeFalsy();
@@ -41,10 +51,30 @@ describe('update_rules_bulk_schema', () => {
expect(
updateRulesBulkSchema.validate>>([
{
- id: 'rule-1',
+ id: 'id-1',
+ risk_score: 50,
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ name: 'some-name',
+ severity: 'low',
+ type: 'query',
+ query: 'some query',
+ index: ['index-1'],
+ interval: '5m',
},
{
- id: 'rule-2',
+ id: 'id-2',
+ risk_score: 50,
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ name: 'some-name',
+ severity: 'low',
+ type: 'query',
+ query: 'some query',
+ index: ['index-1'],
+ interval: '5m',
},
]).error
).toBeFalsy();
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts
index 0dc9f3df3da1ce..c7899f3afa7b8f 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts
@@ -5,169 +5,107 @@
*/
import { updateRulesSchema } from './update_rules_schema';
-import { UpdateRuleAlertParamsRest } from '../../rules/types';
-import { ThreatParams } from '../../types';
+import { PatchRuleAlertParamsRest } from '../../rules/types';
+import { ThreatParams, RuleAlertParamsRest } from '../../types';
-describe('update rules schema', () => {
+describe('create rules schema', () => {
test('empty objects do not validate as they require at least id or rule_id', () => {
- expect(updateRulesSchema.validate>({}).error).toBeTruthy();
+ expect(updateRulesSchema.validate>({}).error).toBeTruthy();
});
test('made up values do not validate', () => {
expect(
- updateRulesSchema.validate>({
+ updateRulesSchema.validate>({
madeUp: 'hi',
}).error
).toBeTruthy();
});
- test('[id] does validate', () => {
+ test('[rule_id] does not validate', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
- }).error
- ).toBeFalsy();
- });
-
- test('[rule_id] does validate', () => {
- expect(
- updateRulesSchema.validate>({
- rule_id: 'rule-1',
- }).error
- ).toBeFalsy();
- });
-
- test('[id and rule_id] does not validate', () => {
- expect(
- updateRulesSchema.validate>({
- id: 'id-1',
+ updateRulesSchema.validate>({
rule_id: 'rule-1',
}).error
).toBeTruthy();
});
- test('[rule_id, description] does validate', () => {
- expect(
- updateRulesSchema.validate>({
- rule_id: 'rule-1',
- description: 'some description',
- }).error
- ).toBeFalsy();
- });
-
- test('[id, description] does validate', () => {
- expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
- description: 'some description',
- }).error
- ).toBeFalsy();
- });
-
- test('[id, risk_score] does validate', () => {
- expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
- risk_score: 10,
- }).error
- ).toBeFalsy();
- });
-
- test('[rule_id, description, from] does validate', () => {
- expect(
- updateRulesSchema.validate>({
- rule_id: 'rule-1',
- description: 'some description',
- from: 'now-5m',
- }).error
- ).toBeFalsy();
- });
-
- test('[id, description, from] does validate', () => {
- expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
- description: 'some description',
- from: 'now-5m',
- }).error
- ).toBeFalsy();
- });
-
- test('[rule_id, description, from, to] does validate', () => {
+ test('[id] and [rule_id] does not validate', () => {
expect(
- updateRulesSchema.validate>({
+ updateRulesSchema.validate>({
+ id: 'id-1',
rule_id: 'rule-1',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
- }).error
- ).toBeFalsy();
+ name: 'some-name',
+ severity: 'low',
+ type: 'query',
+ query: 'some query',
+ index: ['index-1'],
+ interval: '5m',
+ }).error.message
+ ).toEqual('"value" contains a conflict between exclusive peers [id, rule_id]');
});
- test('[id, description, from, to] does validate', () => {
+ test('[rule_id, description] does not validate', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
description: 'some description',
- from: 'now-5m',
- to: 'now',
}).error
- ).toBeFalsy();
+ ).toBeTruthy();
});
- test('[rule_id, description, from, to, name] does validate', () => {
+ test('[rule_id, description, from] does not validate', () => {
expect(
- updateRulesSchema.validate>({
+ updateRulesSchema.validate>({
rule_id: 'rule-1',
description: 'some description',
from: 'now-5m',
- to: 'now',
- name: 'some-name',
}).error
- ).toBeFalsy();
+ ).toBeTruthy();
});
- test('[id, description, from, to, name] does validate', () => {
+ test('[rule_id, description, from, to] does not validate', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
description: 'some description',
from: 'now-5m',
to: 'now',
- name: 'some-name',
}).error
- ).toBeFalsy();
+ ).toBeTruthy();
});
- test('[rule_id, description, from, to, name, severity] does validate', () => {
+ test('[rule_id, description, from, to, name] does not validate', () => {
expect(
- updateRulesSchema.validate>({
+ updateRulesSchema.validate>({
rule_id: 'rule-1',
description: 'some description',
from: 'now-5m',
to: 'now',
name: 'some-name',
- severity: 'low',
}).error
- ).toBeFalsy();
+ ).toBeTruthy();
});
- test('[id, description, from, to, name, severity] does validate', () => {
+ test('[rule_id, description, from, to, name, severity] does not validate', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
description: 'some description',
from: 'now-5m',
to: 'now',
name: 'some-name',
severity: 'low',
}).error
- ).toBeFalsy();
+ ).toBeTruthy();
});
- test('[rule_id, description, from, to, name, severity, type] does validate', () => {
+ test('[rule_id, description, from, to, name, severity, type] does not validate', () => {
expect(
- updateRulesSchema.validate>({
+ updateRulesSchema.validate>({
rule_id: 'rule-1',
description: 'some description',
from: 'now-5m',
@@ -176,56 +114,61 @@ describe('update rules schema', () => {
severity: 'low',
type: 'query',
}).error
- ).toBeFalsy();
+ ).toBeTruthy();
});
- test('[id, description, from, to, name, severity, type] does validate', () => {
+ test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
description: 'some description',
from: 'now-5m',
to: 'now',
name: 'some-name',
severity: 'low',
+ interval: '5m',
type: 'query',
}).error
- ).toBeFalsy();
+ ).toBeTruthy();
});
- test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => {
+ test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => {
expect(
- updateRulesSchema.validate>({
+ updateRulesSchema.validate>({
rule_id: 'rule-1',
description: 'some description',
from: 'now-5m',
to: 'now',
name: 'some-name',
severity: 'low',
- interval: '5m',
type: 'query',
+ interval: '5m',
+ index: ['index-1'],
}).error
- ).toBeFalsy();
+ ).toBeTruthy();
});
- test('[id, description, from, to, name, severity, type, interval] does validate', () => {
+ test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
name: 'some-name',
severity: 'low',
- interval: '5m',
type: 'query',
+ query: 'some query',
+ index: ['index-1'],
+ interval: '5m',
}).error
).toBeFalsy();
});
- test('[rule_id, description, from, to, index, name, severity, interval, type] does validate', () => {
+ test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => {
expect(
- updateRulesSchema.validate>({
+ updateRulesSchema.validate>({
rule_id: 'rule-1',
description: 'some description',
from: 'now-5m',
@@ -235,14 +178,17 @@ describe('update rules schema', () => {
severity: 'low',
interval: '5m',
type: 'query',
+ query: 'some query',
+ language: 'kuery',
}).error
- ).toBeFalsy();
+ ).toBeTruthy();
});
- test('[id, description, from, to, index, name, severity, interval, type] does validate', () => {
+ test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -251,14 +197,18 @@ describe('update rules schema', () => {
severity: 'low',
interval: '5m',
type: 'query',
+ query: 'some query',
+ language: 'kuery',
}).error
).toBeFalsy();
});
- test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => {
+ test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => {
expect(
- updateRulesSchema.validate>({
+ updateRulesSchema.validate>({
rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -268,14 +218,15 @@ describe('update rules schema', () => {
interval: '5m',
type: 'query',
query: 'some query',
+ language: 'kuery',
}).error
).toBeFalsy();
});
- test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => {
+ test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -284,15 +235,17 @@ describe('update rules schema', () => {
severity: 'low',
interval: '5m',
type: 'query',
- query: 'some query',
+ risk_score: 50,
}).error
).toBeFalsy();
});
- test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => {
+ test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => {
expect(
- updateRulesSchema.validate>({
+ updateRulesSchema.validate>({
rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -301,16 +254,16 @@ describe('update rules schema', () => {
severity: 'low',
interval: '5m',
type: 'query',
- query: 'some query',
- language: 'kuery',
}).error
).toBeFalsy();
});
- test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => {
+ test('You can send in an empty array to threat', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -319,16 +272,21 @@ describe('update rules schema', () => {
severity: 'low',
interval: '5m',
type: 'query',
+ references: ['index-1'],
query: 'some query',
language: 'kuery',
+ max_signals: 1,
+ threat: [],
}).error
).toBeFalsy();
});
- test('[rule_id, description, from, to, index, name, severity, type, filter] does validate', () => {
+ test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threat] does validate', () => {
expect(
- updateRulesSchema.validate>({
+ updateRulesSchema.validate>({
rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -337,14 +295,33 @@ describe('update rules schema', () => {
severity: 'low',
interval: '5m',
type: 'query',
+ threat: [
+ {
+ framework: 'someFramework',
+ tactic: {
+ id: 'fakeId',
+ name: 'fakeName',
+ reference: 'fakeRef',
+ },
+ technique: [
+ {
+ id: 'techniqueId',
+ name: 'techniqueName',
+ reference: 'techniqueRef',
+ },
+ ],
+ },
+ ],
}).error
).toBeFalsy();
});
- test('[id, description, from, to, index, name, severity, type, filter] does validate', () => {
+ test('allows references to be sent as valid', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -353,14 +330,19 @@ describe('update rules schema', () => {
severity: 'low',
interval: '5m',
type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
}).error
).toBeFalsy();
});
- test('allows references to be sent as a valid value to update with', () => {
+ test('defaults references to an array', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -369,17 +351,20 @@ describe('update rules schema', () => {
severity: 'low',
interval: '5m',
type: 'query',
- references: ['index-1'],
- query: 'some query',
+ query: 'some-query',
language: 'kuery',
- }).error
- ).toBeFalsy();
+ }).value.references
+ ).toEqual([]);
});
- test('does not default references to an array', () => {
+ test('references cannot be numbers', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate<
+ Partial> & { references: number[] }
+ >({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -390,47 +375,60 @@ describe('update rules schema', () => {
type: 'query',
query: 'some-query',
language: 'kuery',
- }).value.references
- ).toEqual(undefined);
+ references: [5],
+ }).error.message
+ ).toEqual(
+ 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]'
+ );
});
- test('does not default interval', () => {
+ test('indexes cannot be numbers', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
- description: 'some description',
- from: 'now-5m',
- to: 'now',
- index: ['index-1'],
- name: 'some-name',
- severity: 'low',
- type: 'query',
- }).value.interval
- ).toEqual(undefined);
+ updateRulesSchema.validate> & { index: number[] }>(
+ {
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: [5],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ query: 'some-query',
+ language: 'kuery',
+ }
+ ).error.message
+ ).toEqual(
+ 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]'
+ );
});
- test('does not default max signal', () => {
+ test('defaults interval to 5 min', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
index: ['index-1'],
name: 'some-name',
severity: 'low',
- interval: '5m',
type: 'query',
- }).value.max_signals
- ).toEqual(undefined);
+ }).value.interval
+ ).toEqual('5m');
});
- test('references cannot be numbers', () => {
+ test('defaults max signals to 100', () => {
expect(
- updateRulesSchema.validate<
- Partial> & { references: number[] }
- >({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -439,41 +437,34 @@ describe('update rules schema', () => {
severity: 'low',
interval: '5m',
type: 'query',
- query: 'some-query',
- language: 'kuery',
- references: [5],
- }).error.message
- ).toEqual(
- 'child "references" fails because ["references" at position 0 fails because ["0" must be a string]]'
- );
+ }).value.max_signals
+ ).toEqual(100);
});
- test('indexes cannot be numbers', () => {
+ test('saved_id is required when type is saved_query and will not validate without out', () => {
expect(
- updateRulesSchema.validate<
- Partial> & { index: number[] }
- >({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
- index: [5],
+ index: ['index-1'],
name: 'some-name',
severity: 'low',
interval: '5m',
- type: 'query',
- query: 'some-query',
- language: 'kuery',
+ type: 'saved_query',
}).error.message
- ).toEqual(
- 'child "index" fails because ["index" at position 0 fails because ["0" must be a string]]'
- );
+ ).toEqual('child "saved_id" fails because ["saved_id" is required]');
});
- test('saved_id is not required when type is saved_query and will validate without it', () => {
+ test('saved_id is required when type is saved_query and validates with it', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ risk_score: 50,
+ output_index: '.siem-signals',
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -482,14 +473,17 @@ describe('update rules schema', () => {
severity: 'low',
interval: '5m',
type: 'saved_query',
+ saved_id: 'some id',
}).error
).toBeFalsy();
});
- test('saved_id validates with saved_query', () => {
+ test('saved_query type can have filters with it', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -499,14 +493,19 @@ describe('update rules schema', () => {
interval: '5m',
type: 'saved_query',
saved_id: 'some id',
+ filters: [],
}).error
).toBeFalsy();
});
- test('saved_query type can have filters with it', () => {
+ test('filters cannot be a string', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate<
+ Partial & { filters: string }>
+ >({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -516,15 +515,17 @@ describe('update rules schema', () => {
interval: '5m',
type: 'saved_query',
saved_id: 'some id',
- filters: [],
- }).error
- ).toBeFalsy();
+ filters: 'some string',
+ }).error.message
+ ).toEqual('child "filters" fails because ["filters" must be an array]');
});
test('language validates with kuery', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -542,8 +543,10 @@ describe('update rules schema', () => {
test('language validates with lucene', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ risk_score: 50,
+ output_index: '.siem-signals',
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -561,8 +564,10 @@ describe('update rules schema', () => {
test('language does not validate with something made up', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -580,8 +585,10 @@ describe('update rules schema', () => {
test('max_signals cannot be negative', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -600,8 +607,10 @@ describe('update rules schema', () => {
test('max_signals cannot be zero', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -620,8 +629,10 @@ describe('update rules schema', () => {
test('max_signals can be 1', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -638,42 +649,12 @@ describe('update rules schema', () => {
).toBeFalsy();
});
- test('meta can be updated', () => {
- expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
- meta: { whateverYouWant: 'anything_at_all' },
- }).error
- ).toBeFalsy();
- });
-
- test('You cannot update meta as a string', () => {
- expect(
- updateRulesSchema.validate<
- Partial & { meta: string }>
- >({
- id: 'rule-1',
- meta: 'should not work',
- }).error.message
- ).toEqual('child "meta" fails because ["meta" must be an object]');
- });
-
- test('filters cannot be a string', () => {
+ test('You can optionally send in an array of tags', () => {
expect(
- updateRulesSchema.validate<
- Partial & { filters: string }>
- >({
+ updateRulesSchema.validate>({
rule_id: 'rule-1',
- type: 'query',
- filters: 'some string',
- }).error.message
- ).toEqual('child "filters" fails because ["filters" must be an array]');
- });
-
- test('threat is not defaulted to empty array on update', () => {
- expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -686,15 +667,18 @@ describe('update rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: 1,
- }).value.threat
- ).toBe(undefined);
+ tags: ['tag_1', 'tag_2'],
+ }).error
+ ).toBeFalsy();
});
- test('threat is not defaulted to undefined on update with empty array', () => {
+ test('You cannot send in an array of tags that are numbers', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
- description: 'some description',
+ updateRulesSchema.validate> & { tags: number[] }>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
+ description: 'some description',
from: 'now-5m',
to: 'now',
index: ['index-1'],
@@ -706,32 +690,23 @@ describe('update rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: 1,
- threat: [],
- }).value.threat
- ).toMatchObject([]);
- });
-
- test('threat is valid when updated with all sub-objects', () => {
- const expected: ThreatParams[] = [
- {
- framework: 'fake',
- tactic: {
- id: 'fakeId',
- name: 'fakeName',
- reference: 'fakeRef',
- },
- technique: [
- {
- id: 'techniqueId',
- name: 'techniqueName',
- reference: 'techniqueRef',
- },
- ],
- },
- ];
+ tags: [0, 1, 2],
+ }).error.message
+ ).toEqual(
+ 'child "tags" fails because ["tags" at position 0 fails because ["0" must be a string]]'
+ );
+ });
+
+ test('You cannot send in an array of threat that are missing "framework"', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate<
+ Partial> & {
+ threat: Array>>;
+ }
+ >({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -746,7 +721,6 @@ describe('update rules schema', () => {
max_signals: 1,
threat: [
{
- framework: 'fake',
tactic: {
id: 'fakeId',
name: 'fakeName',
@@ -761,18 +735,22 @@ describe('update rules schema', () => {
],
},
],
- }).value.threat
- ).toMatchObject(expected);
+ }).error.message
+ ).toEqual(
+ 'child "threat" fails because ["threat" at position 0 fails because [child "framework" fails because ["framework" is required]]]'
+ );
});
- test('threat is invalid when updated with missing property framework', () => {
+ test('You cannot send in an array of threat that are missing "tactic"', () => {
expect(
updateRulesSchema.validate<
- Partial> & {
- threat: Array>>;
+ Partial> & {
+ threat: Array>>;
}
>({
- id: 'rule-1',
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -787,11 +765,7 @@ describe('update rules schema', () => {
max_signals: 1,
threat: [
{
- tactic: {
- id: 'fakeId',
- name: 'fakeName',
- reference: 'fakeRef',
- },
+ framework: 'fake',
technique: [
{
id: 'techniqueId',
@@ -803,18 +777,20 @@ describe('update rules schema', () => {
],
}).error.message
).toEqual(
- 'child "threat" fails because ["threat" at position 0 fails because [child "framework" fails because ["framework" is required]]]'
+ 'child "threat" fails because ["threat" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]'
);
});
- test('threat is invalid when updated with missing tactic sub-object', () => {
+ test('You cannot send in an array of threat that are missing "technique"', () => {
expect(
updateRulesSchema.validate<
- Partial> & {
- threat: Array>>;
+ Partial> & {
+ threat: Array>>;
}
>({
- id: 'rule-1',
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -830,30 +806,52 @@ describe('update rules schema', () => {
threat: [
{
framework: 'fake',
- technique: [
- {
- id: 'techniqueId',
- name: 'techniqueName',
- reference: 'techniqueRef',
- },
- ],
+ tactic: {
+ id: 'fakeId',
+ name: 'fakeName',
+ reference: 'fakeRef',
+ },
},
],
}).error.message
).toEqual(
- 'child "threat" fails because ["threat" at position 0 fails because [child "tactic" fails because ["tactic" is required]]]'
+ 'child "threat" fails because ["threat" at position 0 fails because [child "technique" fails because ["technique" is required]]]'
);
});
- test('threat is invalid when updated with missing technique', () => {
+ test('You can optionally send in an array of false positives', () => {
+ expect(
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
+ description: 'some description',
+ false_positives: ['false_1', 'false_2'],
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('You cannot send in an array of false positives that are numbers', () => {
expect(
updateRulesSchema.validate<
- Partial> & {
- threat: Array>>;
- }
+ Partial> & { false_positives: number[] }
>({
- id: 'rule-1',
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
+ false_positives: [5, 4],
from: 'now-5m',
to: 'now',
index: ['index-1'],
@@ -865,26 +863,201 @@ describe('update rules schema', () => {
query: 'some query',
language: 'kuery',
max_signals: 1,
- threat: [
- {
- framework: 'fake',
- tactic: {
- id: 'techniqueId',
- name: 'techniqueName',
- reference: 'techniqueRef',
- },
- },
- ],
}).error.message
).toEqual(
- 'child "threat" fails because ["threat" at position 0 fails because [child "technique" fails because ["technique" is required]]]'
+ 'child "false_positives" fails because ["false_positives" at position 0 fails because ["0" must be a string]]'
);
});
+ test('You cannot set the immutable when trying to create a rule', () => {
+ expect(
+ updateRulesSchema.validate<
+ Partial> & { immutable: number }
+ >({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ immutable: 5,
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ }).error.message
+ ).toEqual('"immutable" is not allowed');
+ });
+
+ test('You cannot set the risk_score to 101', () => {
+ expect(
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 101,
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ }).error.message
+ ).toEqual('child "risk_score" fails because ["risk_score" must be less than 101]');
+ });
+
+ test('You cannot set the risk_score to -1', () => {
+ expect(
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: -1,
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ }).error.message
+ ).toEqual('child "risk_score" fails because ["risk_score" must be greater than -1]');
+ });
+
+ test('You can set the risk_score to 0', () => {
+ expect(
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 0,
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('You can set the risk_score to 100', () => {
+ expect(
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 100,
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('You can set meta to any object you want', () => {
+ expect(
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ meta: {
+ somethingMadeUp: { somethingElse: true },
+ },
+ }).error
+ ).toBeFalsy();
+ });
+
+ test('You cannot create meta as a string', () => {
+ expect(
+ updateRulesSchema.validate & { meta: string }>>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ meta: 'should not work',
+ }).error.message
+ ).toEqual('child "meta" fails because ["meta" must be an object]');
+ });
+
+ test('You can omit the query string when filters are present', () => {
+ expect(
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
+ description: 'some description',
+ from: 'now-5m',
+ to: 'now',
+ index: ['index-1'],
+ name: 'some-name',
+ severity: 'low',
+ interval: '5m',
+ type: 'query',
+ references: ['index-1'],
+ language: 'kuery',
+ filters: [],
+ max_signals: 1,
+ }).error
+ ).toBeFalsy();
+ });
+
test('validates with timeline_id and timeline_title', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -892,18 +1065,22 @@ describe('update rules schema', () => {
name: 'some-name',
severity: 'low',
interval: '5m',
- type: 'saved_query',
- saved_id: 'some id',
- timeline_id: 'some-id',
- timeline_title: 'some-title',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ timeline_id: 'timeline-id',
+ timeline_title: 'timeline-title',
}).error
).toBeFalsy();
});
test('You cannot omit timeline_title when timeline_id is present', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -911,17 +1088,21 @@ describe('update rules schema', () => {
name: 'some-name',
severity: 'low',
interval: '5m',
- type: 'saved_query',
- saved_id: 'some id',
- timeline_id: 'some-id',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ timeline_id: 'some_id',
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" is required]');
});
test('You cannot have a null value for timeline_title when timeline_id is present', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -929,9 +1110,11 @@ describe('update rules schema', () => {
name: 'some-name',
severity: 'low',
interval: '5m',
- type: 'saved_query',
- saved_id: 'some id',
- timeline_id: 'timeline-id',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ timeline_id: 'some_id',
timeline_title: null,
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" must be a string]');
@@ -939,8 +1122,10 @@ describe('update rules schema', () => {
test('You cannot have empty string for timeline_title when timeline_id is present', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -948,9 +1133,11 @@ describe('update rules schema', () => {
name: 'some-name',
severity: 'low',
interval: '5m',
- type: 'saved_query',
- saved_id: 'some id',
- timeline_id: 'some-id',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ timeline_id: 'some_id',
timeline_title: '',
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed to be empty]');
@@ -958,8 +1145,10 @@ describe('update rules schema', () => {
test('You cannot have timeline_title with an empty timeline_id', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -967,8 +1156,10 @@ describe('update rules schema', () => {
name: 'some-name',
severity: 'low',
interval: '5m',
- type: 'saved_query',
- saved_id: 'some id',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
timeline_id: '',
timeline_title: 'some-title',
}).error.message
@@ -977,8 +1168,10 @@ describe('update rules schema', () => {
test('You cannot have timeline_title without timeline_id', () => {
expect(
- updateRulesSchema.validate>({
- id: 'rule-1',
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ output_index: '.siem-signals',
+ risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
@@ -986,17 +1179,55 @@ describe('update rules schema', () => {
name: 'some-name',
severity: 'low',
interval: '5m',
- type: 'saved_query',
- saved_id: 'some id',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
timeline_title: 'some-title',
}).error.message
).toEqual('child "timeline_title" fails because ["timeline_title" is not allowed]');
});
+ test('The default for "from" will be "now-6m"', () => {
+ expect(
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ risk_score: 50,
+ description: 'some description',
+ name: 'some-name',
+ severity: 'low',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ version: 1,
+ }).value.from
+ ).toEqual('now-6m');
+ });
+
+ test('The default for "to" will be "now"', () => {
+ expect(
+ updateRulesSchema.validate>({
+ rule_id: 'rule-1',
+ risk_score: 50,
+ description: 'some description',
+ name: 'some-name',
+ severity: 'low',
+ type: 'query',
+ references: ['index-1'],
+ query: 'some query',
+ language: 'kuery',
+ max_signals: 1,
+ version: 1,
+ }).value.to
+ ).toEqual('now');
+ });
+
test('You cannot set the severity to a value other than low, medium, high, or critical', () => {
expect(
- updateRulesSchema.validate