[Security Solution] Migrate Rules schema to OpenAPI (#167999)

**Parent meta ticket:
https://github.com/elastic/security-team/issues/7491**

Resolves: https://github.com/elastic/security-team/issues/7582
Resolves: https://github.com/elastic/security-team/issues/7580
Resolves: https://github.com/elastic/security-team/issues/7581

## Summary

This PR migrates the rules schema to OpenAPI, Zod, and code generation.

The following APIs now have complete OpenAPI specifications and are
enabled for code generation:

| Method | Endpoint | OpenAPI spec | Fully migrated |
| ------ |
---------------------------------------------------------------- |
------------ | -------------- |
| POST | /api/detection_engine/rules |  |  |
| GET | /api/detection_engine/rules |  |  |
| PUT | /api/detection_engine/rules |  |  |
| PATCH | /api/detection_engine/rules |  |  |
| DELETE | /api/detection_engine/rules |  |  |
| POST | /api/detection_engine/rules/\_bulk_create |  |  |
| PUT | /api/detection_engine/rules/\_bulk_update |  |  |
| PATCH | /api/detection_engine/rules/\_bulk_update |  |  |
| DELETE | /api/detection_engine/rules/\_bulk_delete |  |  |
| POST | /api/detection_engine/rules/\_bulk_delete |  |  |

### Rule schemas are now forward-compatible

We now allow extra fields in schemas for forward compatibility, but we
remove them from the payload during parsing. So from now on, extra
fields are simply ignored and won't lead to validation errors.
This commit is contained in:
Dmitrii Shevchenko 2023-10-27 18:00:10 +02:00 committed by GitHub
parent 46ca1f08b7
commit 3a18916b31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
250 changed files with 5671 additions and 5813 deletions

3
.github/CODEOWNERS vendored
View file

@ -553,7 +553,7 @@ x-pack/plugins/observability @elastic/actionable-observability
x-pack/plugins/observability_shared @elastic/observability-ui
x-pack/test/security_api_integration/plugins/oidc_provider @elastic/kibana-security
test/common/plugins/otel_metrics @elastic/infra-monitoring-ui
packages/kbn-openapi-generator @elastic/security-detection-engine
packages/kbn-openapi-generator @elastic/security-detection-rule-management
packages/kbn-optimizer @elastic/kibana-operations
packages/kbn-optimizer-webpack-helpers @elastic/kibana-operations
packages/kbn-osquery-io-ts-types @elastic/security-asset-management
@ -828,6 +828,7 @@ packages/kbn-web-worker-stub @elastic/kibana-operations
packages/kbn-whereis-pkg-cli @elastic/kibana-operations
packages/kbn-xstate-utils @elastic/infra-monitoring-ui
packages/kbn-yarn-lock-validator @elastic/kibana-operations
packages/kbn-zod-helpers @elastic/security-detection-rule-management
####
## Everything below this line overrides the default assignments for each package.
## Items lower in the file have higher precedence:

View file

@ -815,6 +815,7 @@
"@kbn/visualizations-plugin": "link:src/plugins/visualizations",
"@kbn/watcher-plugin": "link:x-pack/plugins/watcher",
"@kbn/xstate-utils": "link:packages/kbn-xstate-utils",
"@kbn/zod-helpers": "link:packages/kbn-zod-helpers",
"@loaders.gl/core": "^3.4.7",
"@loaders.gl/json": "^3.4.7",
"@loaders.gl/shapefile": "^3.4.7",

View file

@ -1,6 +1,6 @@
{
"devOnly": true,
"id": "@kbn/openapi-generator",
"owner": "@elastic/security-detection-engine",
"owner": "@elastic/security-detection-rule-management",
"type": "shared-common"
}

View file

@ -1,7 +1,4 @@
{
"bin": {
"openapi-generator": "./bin/openapi-generator.js"
},
"description": "OpenAPI code generator for Kibana",
"license": "SSPL-1.0 OR Elastic License 2.0",
"name": "@kbn/openapi-generator",

View file

@ -35,6 +35,12 @@ export function registerHelpers(handlebarsInstance: typeof Handlebars) {
handlebarsInstance.registerHelper('defined', (val) => {
return val !== undefined;
});
handlebarsInstance.registerHelper('first', (val) => {
return Array.isArray(val) ? val[0] : val;
});
handlebarsInstance.registerHelper('isSingle', (val) => {
return Array.isArray(val) && val.length === 1;
});
/**
* Check if the OpenAPI schema is unknown
*/

View file

@ -6,6 +6,7 @@
*/
import { z } from "zod";
import { requiredOptional, isValidDateMath } from "@kbn/zod-helpers"
{{> disclaimer}}
@ -24,8 +25,10 @@ import {
export type {{@key}} = z.infer<typeof {{@key}}>;
export const {{@key}} = {{> zod_schema_item}};
{{#if enum}}
export const {{@key}}Enum = {{@key}}.enum;
{{#unless (isSingle enum)}}
export type {{@key}}Enum = typeof {{@key}}.enum;
export const {{@key}}Enum = {{@key}}.enum;
{{/unless}}
{{/if}}
{{/each}}

View file

@ -10,6 +10,9 @@
{{~#if nullable}}.nullable(){{/if~}}
{{~#if (eq requiredBool false)}}.optional(){{/if~}}
{{~#if (defined default)}}.default({{{toJSON default}}}){{/if~}}
{{~#if (eq x-modify "partial")}}.partial(){{/if~}}
{{~#if (eq x-modify "required")}}.required(){{/if~}}
{{~#if (eq x-modify "requiredOptional")}}.transform(requiredOptional){{/if~}}
{{~/if~}}
{{~#if allOf~}}
@ -28,6 +31,8 @@
{{~> zod_schema_item ~}},
{{~/each~}}
])
{{~#if nullable}}.nullable(){{/if~}}
{{~#if (eq requiredBool false)}}.optional(){{/if~}}
{{~/if~}}
{{~#if oneOf~}}
@ -36,6 +41,8 @@
{{~> zod_schema_item ~}},
{{~/each~}}
])
{{~#if nullable}}.nullable(){{/if~}}
{{~#if (eq requiredBool false)}}.optional(){{/if~}}
{{~/if~}}
{{#if (isUnknown .)}}
@ -76,22 +83,38 @@ z.unknown()
{{@key}}: {{> zod_schema_item requiredBool=(includes ../required @key)}},
{{/each}}
})
{{#if (eq additionalProperties false)}}.strict(){{/if}}
{{~#if (eq additionalProperties false)}}.strict(){{/if~}}
{{~#if additionalProperties}}
{{~#if (eq additionalProperties true)~}}
.catchall(z.unknown())
{{~else~}}
.catchall({{> zod_schema_item additionalProperties}})
{{~/if~}}
{{~/if~}}
{{~#if (eq x-modify "partial")}}.partial(){{/if~}}
{{~#if (eq x-modify "required")}}.required(){{/if~}}
{{~#if (eq x-modify "requiredOptional")}}.transform(requiredOptional){{/if~}}
{{~/inline~}}
{{~#*inline "type_string"~}}
{{~#if enum~}}
z.enum([
{{~#each enum~}}
"{{.}}",
{{~/each~}}
])
{{~#if (isSingle enum)~}}
z.literal("{{first enum}}")
{{~else~}}
z.enum([
{{~#each enum~}}
"{{.}}",
{{~/each~}}
])
{{~/if~}}
{{~else~}}
z.string()
{{~#if minLength}}.min({{minLength}}){{/if~}}
{{~#if maxLength}}.max({{maxLength}}){{/if~}}
{{~#if (eq format 'date-time')}}.datetime(){{/if~}}
{{~#if (eq format 'date-math')}}.superRefine(isValidDateMath){{/if~}}
{{~#if (eq format 'uuid')}}.uuid(){{/if~}}
{{~#if pattern}}.regex(/{{pattern}}/){{/if~}}
{{~/if~}}
{{#if transform}}.transform({{{transform}}}){{/if~}}
{{~/inline~}}

View file

@ -6,8 +6,9 @@
* Side Public License, v 1.
*/
import Boom from '@hapi/boom';
import { errors } from '@elastic/elasticsearch';
import Boom from '@hapi/boom';
import { stringifyZodError } from '@kbn/zod-helpers';
import { ZodError } from 'zod';
import { BadRequestError } from '../bad_request_error';
@ -60,15 +61,3 @@ export const transformError = (err: Error & Partial<errors.ResponseError>): Outp
}
}
};
export function stringifyZodError(err: ZodError<any>) {
return err.issues
.map((issue) => {
// If the path is empty, the error is for the root object
if (issue.path.length === 0) {
return issue.message;
}
return `${issue.path.join('.')}: ${issue.message}`;
})
.join(', ');
}

View file

@ -12,5 +12,8 @@
],
"exclude": [
"target/**/*",
],
"kbn_references": [
"@kbn/zod-helpers",
]
}

View file

@ -0,0 +1,14 @@
# Helpers and utilities for Zod
[Zod](https://zod.dev/) is a schema validation library with static type inference for TypeScript.
Helpers defined in this package:
- Can be used in other packages and plugins to make it easier to define schemas with Zod, such as API schemas.
- Are already used in `packages/kbn-openapi-generator`.
- Are already used in `x-pack/plugins/security_solution`.
When you add some helper code to this package, please make sure that:
- The code is generic and domain-agnostic (doesn't "know" about any domains such as Security or Observability).
- The code is reusable and there are already a few use cases for it. Try to not generalize prematurely.

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './src/expect_parse_error';
export * from './src/expect_parse_success';
export * from './src/is_valid_date_math';
export * from './src/required_optional';
export * from './src/stringify_zod_error';

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-zod-helpers'],
};

View file

@ -0,0 +1,6 @@
{
"devOnly": false,
"id": "@kbn/zod-helpers",
"owner": "@elastic/security-detection-rule-management",
"type": "shared-common"
}

View file

@ -0,0 +1,7 @@
{
"description": "Zod helpers for Kibana",
"license": "SSPL-1.0 OR Elastic License 2.0",
"name": "@kbn/zod-helpers",
"private": true,
"version": "1.0.0"
}

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { SafeParseError, SafeParseReturnType } from 'zod';
export function expectParseError<Input, Output>(
result: SafeParseReturnType<Input, Output>
): asserts result is SafeParseError<Input> {
expect(result.success).toEqual(false);
}

View file

@ -1,17 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { SafeParseError, SafeParseReturnType, SafeParseSuccess } from 'zod';
export function expectParseError<Input, Output>(
result: SafeParseReturnType<Input, Output>
): asserts result is SafeParseError<Input> {
expect(result.success).toEqual(false);
}
import type { SafeParseReturnType, SafeParseSuccess } from 'zod';
export function expectParseSuccess<Input, Output>(
result: SafeParseReturnType<Input, Output>

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as z from 'zod';
import dateMath from '@kbn/datemath';
function validateDateMath(time: string): boolean {
const isValidDateString = !isNaN(Date.parse(time));
if (isValidDateString) {
return true;
}
const isDateMath = time.trim().startsWith('now');
if (isDateMath) {
return Boolean(dateMath.parse(time));
}
return false;
}
export function isValidDateMath(input: string, ctx: z.RefinementCtx) {
if (!validateDateMath(input)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Failed to parse date-math expression',
});
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/**
* Make any optional fields required, but add `| undefined` to their type.
*
* This bit of logic is to force all fields to be accounted for in conversions
* from the internal rule schema to the response schema. Rather than use
* partial, which makes each field optional, we make each field required but
* possibly undefined. The result is that if a field is forgotten in the
* conversion from internal schema to response schema TS will report an error.
* If we just used partial instead, then optional fields can be accidentally
* omitted from the conversion - and any actual values in those fields
* internally will be stripped in the response.
*
* @example
* type A = RequiredOptional<{ a?: string; b: number }>;
* will yield a type of: type A = { a: string | undefined; b: number; }
*
* @note
* We should consider removing this logic altogether from our schemas and use it
* in place with converters whenever needed.
*/
export type RequiredOptional<T> = { [K in keyof T]-?: [T[K]] } extends infer U
? U extends Record<keyof U, [unknown]>
? { [K in keyof U]: U[K][0] }
: never
: never;
/**
* This helper designed to be used with `z.transform` to make all optional fields required.
*
* @param schema Zod schema
* @returns The same schema but with all optional fields required.
*/
export const requiredOptional = <T>(schema: T) => schema as RequiredOptional<T>;

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ZodError } from 'zod';
export function stringifyZodError(err: ZodError<any>) {
return err.issues
.map((issue) => {
// If the path is empty, the error is for the root object
if (issue.path.length === 0) {
return issue.message;
}
return `${issue.path.join('.')}: ${issue.message}`;
})
.join(', ');
}

View file

@ -0,0 +1,12 @@
{
"compilerOptions": {
"outDir": "target/types",
"types": ["jest", "node"]
},
"exclude": ["target/**/*"],
"extends": "../../tsconfig.base.json",
"include": ["**/*.ts"],
"kbn_references": [
"@kbn/datemath",
]
}

View file

@ -1650,6 +1650,8 @@
"@kbn/xstate-utils/*": ["packages/kbn-xstate-utils/*"],
"@kbn/yarn-lock-validator": ["packages/kbn-yarn-lock-validator"],
"@kbn/yarn-lock-validator/*": ["packages/kbn-yarn-lock-validator/*"],
"@kbn/zod-helpers": ["packages/kbn-zod-helpers"],
"@kbn/zod-helpers/*": ["packages/kbn-zod-helpers/*"],
// END AUTOMATED PACKAGE LISTING
// Allows for importing from `kibana` package for the exported types.
"@emotion/core": [

View file

@ -70,7 +70,8 @@ import type {
ALERT_RULE_TIMESTAMP_OVERRIDE,
} from '../../../../../field_maps/field_names';
// TODO: Create and import 8.0.0 versioned RuleAlertAction type
import type { RuleAlertAction, SearchTypes } from '../../../../../detection_engine/types';
import type { SearchTypes } from '../../../../../detection_engine/types';
import type { RuleAction } from '../../rule_schema';
/* DO NOT MODIFY THIS SCHEMA TO ADD NEW FIELDS. These types represent the alerts that shipped in 8.0.0.
Any changes to these types should be bug fixes so the types more accurately represent the alerts from 8.0.0.
@ -110,7 +111,7 @@ export interface BaseFields800 {
[ALERT_RISK_SCORE]: number;
// TODO: version rule schemas and pull in 8.0.0 versioned rule schema to define alert rule parameters type
[ALERT_RULE_PARAMETERS]: { [key: string]: SearchTypes };
[ALERT_RULE_ACTIONS]: RuleAlertAction[];
[ALERT_RULE_ACTIONS]: RuleAction[];
[ALERT_RULE_AUTHOR]: string[];
[ALERT_RULE_CREATED_AT]: string;
[ALERT_RULE_CREATED_BY]: string;

View file

@ -4,8 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ErrorSchema } from './error_schema';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
import type { ErrorSchema } from './error_schema_legacy';
export const getErrorSchemaMock = (
id: string = '819eded6-e9c8-445b-a647-519aea39e063'

View file

@ -8,7 +8,9 @@
import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
import { left } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import { ErrorSchema } from './error_schema';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
import { ErrorSchema } from './error_schema_legacy';
import { getErrorSchemaMock } from './error_schema.mock';
describe('error_schema', () => {

View file

@ -8,7 +8,10 @@
import { NonEmptyString } from '@kbn/securitysolution-io-ts-types';
import * as t from 'io-ts';
import { RuleSignatureId } from './rule_schema';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
import { RuleSignatureId } from './rule_schema_legacy';
import { status_code, message } from './schemas';
// We use id: t.string intentionally and _never_ the id from global schemas as

View file

@ -8,8 +8,12 @@
export * from './alerts';
export * from './rule_response_actions';
export * from './rule_schema';
export * from './error_schema';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
export * from './error_schema_legacy';
export * from './pagination';
export * from './schemas';
export * from './sorting';
export * from './warning_schema';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
export * from './sorting_legacy';
export * from './warning_schema.gen';

View file

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS } from '../../../../endpoint/service/response_actions/constants';
// to enable using RESPONSE_ACTION_API_COMMANDS_NAMES as a type
function keyObject<T extends readonly string[]>(arr: T): { [K in T[number]]: null } {
return Object.fromEntries(arr.map((v) => [v, null])) as never;
}
export const EndpointParams = t.type({
command: t.keyof(keyObject(ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS)),
comment: t.union([t.string, t.undefined]),
});
export type EndpointParams = t.TypeOf<typeof EndpointParams>;

View file

@ -5,6 +5,7 @@
* 2.0.
*/
export * from './response_actions';
export * from './endpoint';
export * from './osquery';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
export { RESPONSE_ACTION_TYPES, SUPPORTED_RESPONSE_ACTION_TYPES } from './response_actions_legacy';
export * from './response_actions.gen';

View file

@ -1,25 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { ecsMapping, arrayQueries } from '@kbn/osquery-io-ts-types';
export const OsqueryParams = t.type({
query: t.union([t.string, t.undefined]),
ecs_mapping: t.union([ecsMapping, t.undefined]),
queries: t.union([arrayQueries, t.undefined]),
pack_id: t.union([t.string, t.undefined]),
saved_query_id: t.union([t.string, t.undefined]),
});
export const OsqueryParamsCamelCase = t.type({
query: t.union([t.string, t.undefined]),
ecsMapping: t.union([ecsMapping, t.undefined]),
queries: t.union([arrayQueries, t.undefined]),
packId: t.union([t.string, t.undefined]),
savedQueryId: t.union([t.string, t.undefined]),
});

View file

@ -0,0 +1,109 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from 'zod';
import { requiredOptional } from '@kbn/zod-helpers';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
export type ResponseActionTypes = z.infer<typeof ResponseActionTypes>;
export const ResponseActionTypes = z.enum(['.osquery', '.endpoint']);
export type ResponseActionTypesEnum = typeof ResponseActionTypes.enum;
export const ResponseActionTypesEnum = ResponseActionTypes.enum;
export type EcsMapping = z.infer<typeof EcsMapping>;
export const EcsMapping = z.object({}).catchall(
z.object({
field: z.string().optional(),
value: z.union([z.string(), z.array(z.string())]).optional(),
})
);
export type OsqueryQuery = z.infer<typeof OsqueryQuery>;
export const OsqueryQuery = z
.object({
/**
* Query ID
*/
id: z.string(),
/**
* Query to execute
*/
query: z.string(),
ecs_mapping: EcsMapping.optional(),
/**
* Query version
*/
version: z.string().optional(),
platform: z.string().optional(),
removed: z.boolean().optional(),
snapshot: z.boolean().optional(),
})
.transform(requiredOptional);
export type OsqueryParams = z.infer<typeof OsqueryParams>;
export const OsqueryParams = z
.object({
query: z.string().optional(),
ecs_mapping: EcsMapping.optional(),
queries: z.array(OsqueryQuery).optional(),
pack_id: z.string().optional(),
saved_query_id: z.string().optional(),
})
.transform(requiredOptional);
export type OsqueryParamsCamelCase = z.infer<typeof OsqueryParamsCamelCase>;
export const OsqueryParamsCamelCase = z
.object({
query: z.string().optional(),
ecsMapping: EcsMapping.optional(),
queries: z.array(OsqueryQuery).optional(),
packId: z.string().optional(),
savedQueryId: z.string().optional(),
})
.transform(requiredOptional);
export type OsqueryResponseAction = z.infer<typeof OsqueryResponseAction>;
export const OsqueryResponseAction = z.object({
action_type_id: z.literal('.osquery'),
params: OsqueryParams,
});
export type RuleResponseOsqueryAction = z.infer<typeof RuleResponseOsqueryAction>;
export const RuleResponseOsqueryAction = z.object({
actionTypeId: z.literal('.osquery'),
params: OsqueryParamsCamelCase,
});
export type EndpointParams = z.infer<typeof EndpointParams>;
export const EndpointParams = z
.object({
command: z.literal('isolate'),
comment: z.string().optional(),
})
.transform(requiredOptional);
export type EndpointResponseAction = z.infer<typeof EndpointResponseAction>;
export const EndpointResponseAction = z.object({
action_type_id: z.literal('.endpoint'),
params: EndpointParams,
});
export type RuleResponseEndpointAction = z.infer<typeof RuleResponseEndpointAction>;
export const RuleResponseEndpointAction = z.object({
actionTypeId: z.literal('.endpoint'),
params: EndpointParams,
});
export type ResponseAction = z.infer<typeof ResponseAction>;
export const ResponseAction = z.union([OsqueryResponseAction, EndpointResponseAction]);
export type RuleResponseAction = z.infer<typeof RuleResponseAction>;
export const RuleResponseAction = z.union([RuleResponseOsqueryAction, RuleResponseEndpointAction]);

View file

@ -0,0 +1,164 @@
openapi: 3.0.0
info:
title: Response Actions Schema
version: 'not applicable'
paths: {}
components:
x-codegen-enabled: true
schemas:
ResponseActionTypes:
type: string
enum:
- .osquery
- .endpoint
EcsMapping:
type: object
additionalProperties:
type: object
properties:
field:
type: string
value:
oneOf:
- type: string
- type: array
items:
type: string
OsqueryQuery:
type: object
properties:
id:
type: string
description: Query ID
query:
type: string
description: Query to execute
ecs_mapping:
$ref: '#/components/schemas/EcsMapping'
version:
type: string
description: Query version
platform:
type: string
removed:
type: boolean
snapshot:
type: boolean
required:
- id
- query
x-modify: requiredOptional
OsqueryParams:
type: object
properties:
query:
type: string
ecs_mapping:
$ref: '#/components/schemas/EcsMapping'
queries:
type: array
items:
$ref: '#/components/schemas/OsqueryQuery'
pack_id:
type: string
saved_query_id:
type: string
x-modify: requiredOptional
OsqueryParamsCamelCase:
type: object
properties:
query:
type: string
ecsMapping:
$ref: '#/components/schemas/EcsMapping'
queries:
type: array
items:
$ref: '#/components/schemas/OsqueryQuery'
packId:
type: string
savedQueryId:
type: string
x-modify: requiredOptional
OsqueryResponseAction:
type: object
properties:
action_type_id:
type: string
enum:
- .osquery
params:
$ref: '#/components/schemas/OsqueryParams'
required:
- action_type_id
- params
# Camel cased versions of OsqueryResponseAction
RuleResponseOsqueryAction:
type: object
properties:
actionTypeId:
type: string
enum:
- .osquery
params:
$ref: '#/components/schemas/OsqueryParamsCamelCase'
required:
- actionTypeId
- params
EndpointParams:
type: object
properties:
command:
type: string
enum:
- isolate
comment:
type: string
required:
- command
x-modify: requiredOptional
EndpointResponseAction:
type: object
properties:
action_type_id:
type: string
enum:
- .endpoint
params:
$ref: '#/components/schemas/EndpointParams'
required:
- action_type_id
- params
# Camel cased versions of EndpointResponseAction
RuleResponseEndpointAction:
type: object
properties:
actionTypeId:
type: string
enum:
- .endpoint
params:
$ref: '#/components/schemas/EndpointParams'
required:
- actionTypeId
- params
ResponseAction:
oneOf:
- $ref: '#/components/schemas/OsqueryResponseAction'
- $ref: '#/components/schemas/EndpointResponseAction'
# Camel Cased versions of ResponseAction
RuleResponseAction:
oneOf:
- $ref: '#/components/schemas/RuleResponseOsqueryAction'
- $ref: '#/components/schemas/RuleResponseEndpointAction'

View file

@ -1,60 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { EndpointParams } from './endpoint';
import { OsqueryParams, OsqueryParamsCamelCase } from './osquery';
export enum RESPONSE_ACTION_TYPES {
OSQUERY = '.osquery',
ENDPOINT = '.endpoint',
}
export const SUPPORTED_RESPONSE_ACTION_TYPES = Object.values(RESPONSE_ACTION_TYPES);
// When we create new response action types, create a union of types
export const OsqueryResponseActionRuleParam = t.strict({
actionTypeId: t.literal(RESPONSE_ACTION_TYPES.OSQUERY),
params: OsqueryParamsCamelCase,
});
export type RuleResponseOsqueryAction = t.TypeOf<typeof OsqueryResponseActionRuleParam>;
export const EndpointResponseActionRuleParam = t.strict({
actionTypeId: t.literal(RESPONSE_ACTION_TYPES.ENDPOINT),
params: EndpointParams,
});
export type RuleResponseEndpointAction = t.TypeOf<typeof EndpointResponseActionRuleParam>;
const ResponseActionRuleParam = t.union([
OsqueryResponseActionRuleParam,
EndpointResponseActionRuleParam,
]);
export type RuleResponseAction = t.TypeOf<typeof ResponseActionRuleParam>;
export const ResponseActionRuleParamsOrUndefined = t.union([
t.array(ResponseActionRuleParam),
t.undefined,
]);
// When we create new response action types, create a union of types
const OsqueryResponseAction = t.strict({
action_type_id: t.literal(RESPONSE_ACTION_TYPES.OSQUERY),
params: OsqueryParams,
});
const EndpointResponseAction = t.strict({
action_type_id: t.literal(RESPONSE_ACTION_TYPES.ENDPOINT),
params: EndpointParams,
});
const ResponseAction = t.union([OsqueryResponseAction, EndpointResponseAction]);
export const ResponseActionArray = t.array(ResponseAction);
export type ResponseAction = t.TypeOf<typeof ResponseAction>;

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { arrayQueries, ecsMapping } from '@kbn/osquery-io-ts-types';
import * as t from 'io-ts';
import { ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS } from '../../../../endpoint/service/response_actions/constants';
import { ResponseActionTypesEnum } from './response_actions.gen';
export const RESPONSE_ACTION_TYPES = {
OSQUERY: ResponseActionTypesEnum['.osquery'],
ENDPOINT: ResponseActionTypesEnum['.endpoint'],
} as const;
export const SUPPORTED_RESPONSE_ACTION_TYPES = Object.values(RESPONSE_ACTION_TYPES);
// to enable using RESPONSE_ACTION_API_COMMANDS_NAMES as a type
function keyObject<T extends readonly string[]>(arr: T): { [K in T[number]]: null } {
return Object.fromEntries(arr.map((v) => [v, null])) as never;
}
export type EndpointParams = t.TypeOf<typeof EndpointParams>;
export const EndpointParams = t.type({
command: t.keyof(keyObject(ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS)),
comment: t.union([t.string, t.undefined]),
});
export const OsqueryParams = t.type({
query: t.union([t.string, t.undefined]),
ecs_mapping: t.union([ecsMapping, t.undefined]),
queries: t.union([arrayQueries, t.undefined]),
pack_id: t.union([t.string, t.undefined]),
saved_query_id: t.union([t.string, t.undefined]),
});
export const OsqueryParamsCamelCase = t.type({
query: t.union([t.string, t.undefined]),
ecsMapping: t.union([ecsMapping, t.undefined]),
queries: t.union([arrayQueries, t.undefined]),
packId: t.union([t.string, t.undefined]),
savedQueryId: t.union([t.string, t.undefined]),
});
// When we create new response action types, create a union of types
export type RuleResponseOsqueryAction = t.TypeOf<typeof RuleResponseOsqueryAction>;
export const RuleResponseOsqueryAction = t.strict({
actionTypeId: t.literal(RESPONSE_ACTION_TYPES.OSQUERY),
params: OsqueryParamsCamelCase,
});
export type RuleResponseEndpointAction = t.TypeOf<typeof RuleResponseEndpointAction>;
export const RuleResponseEndpointAction = t.strict({
actionTypeId: t.literal(RESPONSE_ACTION_TYPES.ENDPOINT),
params: EndpointParams,
});
export type RuleResponseAction = t.TypeOf<typeof ResponseActionRuleParam>;
const ResponseActionRuleParam = t.union([RuleResponseOsqueryAction, RuleResponseEndpointAction]);
export const ResponseActionRuleParamsOrUndefined = t.union([
t.array(ResponseActionRuleParam),
t.undefined,
]);
// When we create new response action types, create a union of types
const OsqueryResponseAction = t.strict({
action_type_id: t.literal(RESPONSE_ACTION_TYPES.OSQUERY),
params: OsqueryParams,
});
const EndpointResponseAction = t.strict({
action_type_id: t.literal(RESPONSE_ACTION_TYPES.ENDPOINT),
params: EndpointParams,
});
export type ResponseAction = t.TypeOf<typeof ResponseAction>;
export const ResponseAction = t.union([OsqueryResponseAction, EndpointResponseAction]);
export const ResponseActionArray = t.array(ResponseAction);

View file

@ -1,95 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
interface RuleFields<
Required extends t.Props,
Optional extends t.Props,
Defaultable extends t.Props
> {
required: Required;
optional: Optional;
defaultable: Defaultable;
}
export const buildRuleSchemas = <R extends t.Props, O extends t.Props, D extends t.Props>(
fields: RuleFields<R, O, D>
) => {
return {
...fields,
create: buildCreateRuleSchema(fields.required, fields.optional, fields.defaultable),
patch: buildPatchRuleSchema(fields.required, fields.optional, fields.defaultable),
response: buildResponseRuleSchema(fields.required, fields.optional, fields.defaultable),
};
};
const buildCreateRuleSchema = <
Required extends t.Props,
Optional extends t.Props,
Defaultable extends t.Props
>(
requiredFields: Required,
optionalFields: Optional,
defaultableFields: Defaultable
) => {
return t.intersection([
t.exact(t.type(requiredFields)),
t.exact(t.partial(optionalFields)),
t.exact(t.partial(defaultableFields)),
]);
};
const buildPatchRuleSchema = <
Required extends t.Props,
Optional extends t.Props,
Defaultable extends t.Props
>(
requiredFields: Required,
optionalFields: Optional,
defaultableFields: Defaultable
) => {
return t.intersection([
t.partial(requiredFields),
t.partial(optionalFields),
t.partial(defaultableFields),
]);
};
export type OrUndefined<P extends t.Props> = {
[K in keyof P]: P[K] | t.UndefinedC;
};
export const orUndefined = <P extends t.Props>(props: P): OrUndefined<P> => {
return Object.keys(props).reduce<t.Props>((acc, key) => {
acc[key] = t.union([props[key], t.undefined]);
return acc;
}, {}) as OrUndefined<P>;
};
export const buildResponseRuleSchema = <
Required extends t.Props,
Optional extends t.Props,
Defaultable extends t.Props
>(
requiredFields: Required,
optionalFields: Optional,
defaultableFields: Defaultable
) => {
// This bit of logic is to force all fields to be accounted for in conversions from the internal
// rule schema to the response schema. Rather than use `t.partial`, which makes each field optional,
// we make each field required but possibly undefined. The result is that if a field is forgotten in
// the conversion from internal schema to response schema TS will report an error. If we just used t.partial
// instead, then optional fields can be accidentally omitted from the conversion - and any actual values
// in those fields internally will be stripped in the response.
const optionalWithUndefined = orUndefined(optionalFields);
return t.intersection([
t.exact(t.type(requiredFields)),
t.exact(t.type(optionalWithUndefined)),
t.exact(t.type(defaultableFields)),
]);
};

View file

@ -6,20 +6,30 @@
*/
import { z } from 'zod';
import { requiredOptional, isValidDateMath } from '@kbn/zod-helpers';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
/**
* A string that is not empty and does not contain only whitespace
*/
export type NonEmptyString = z.infer<typeof NonEmptyString>;
export const NonEmptyString = z
.string()
.min(1)
.regex(/^(?! *$).+$/);
/**
* A universally unique identifier
*/
export type UUID = z.infer<typeof UUID>;
export const UUID = z.string();
export const UUID = z.string().uuid();
export type RuleObjectId = z.infer<typeof RuleObjectId>;
export const RuleObjectId = z.string();
export const RuleObjectId = UUID;
/**
* Could be any string, not necessarily a UUID
@ -33,21 +43,100 @@ export const RuleName = z.string().min(1);
export type RuleDescription = z.infer<typeof RuleDescription>;
export const RuleDescription = z.string().min(1);
/**
* The rule's version number.
*/
export type RuleVersion = z.infer<typeof RuleVersion>;
export const RuleVersion = z.string();
export const RuleVersion = z.number().int().min(1);
export type QueryLanguage = z.infer<typeof QueryLanguage>;
export const QueryLanguage = z.enum(['kuery', 'lucene', 'eql', 'esql']);
export type QueryLanguageEnum = typeof QueryLanguage.enum;
export const QueryLanguageEnum = QueryLanguage.enum;
export type KqlQueryLanguage = z.infer<typeof KqlQueryLanguage>;
export const KqlQueryLanguage = z.enum(['kuery', 'lucene']);
export type KqlQueryLanguageEnum = typeof KqlQueryLanguage.enum;
export const KqlQueryLanguageEnum = KqlQueryLanguage.enum;
export type IsRuleImmutable = z.infer<typeof IsRuleImmutable>;
export const IsRuleImmutable = z.boolean();
/**
* Determines whether the rule is enabled.
*/
export type IsRuleEnabled = z.infer<typeof IsRuleEnabled>;
export const IsRuleEnabled = z.boolean();
/**
* Frequency of rule execution, using a date math range. For example, "1h" means the rule runs every hour. Defaults to 5m (5 minutes).
*/
export type RuleInterval = z.infer<typeof RuleInterval>;
export const RuleInterval = z.string();
/**
* Time from which data is analyzed each time the rule executes, using a date math range. For example, now-4200s means the rule analyzes data from 70 minutes before its start time. Defaults to now-6m (analyzes data from 6 minutes before the start time).
*/
export type RuleIntervalFrom = z.infer<typeof RuleIntervalFrom>;
export const RuleIntervalFrom = z.string().superRefine(isValidDateMath);
export type RuleIntervalTo = z.infer<typeof RuleIntervalTo>;
export const RuleIntervalTo = z.string();
/**
* Risk score (0 to 100)
*/
export type RiskScore = z.infer<typeof RiskScore>;
export const RiskScore = z.number().int().min(0).max(100);
/**
* Overrides generated alerts' risk_score with a value from the source event
*/
export type RiskScoreMapping = z.infer<typeof RiskScoreMapping>;
export const RiskScoreMapping = z.array(
z
.object({
field: z.string(),
operator: z.literal('equals'),
value: z.string(),
risk_score: RiskScore.optional(),
})
.transform(requiredOptional)
);
/**
* Severity of the rule
*/
export type Severity = z.infer<typeof Severity>;
export const Severity = z.enum(['low', 'medium', 'high', 'critical']);
export type SeverityEnum = typeof Severity.enum;
export const SeverityEnum = Severity.enum;
/**
* Overrides generated alerts' severity with values from the source event
*/
export type SeverityMapping = z.infer<typeof SeverityMapping>;
export const SeverityMapping = z.array(
z.object({
field: z.string(),
operator: z.literal('equals'),
severity: Severity,
value: z.string(),
})
);
/**
* String array containing words and phrases to help categorize, filter, and search rules. Defaults to an empty array.
*/
export type RuleTagArray = z.infer<typeof RuleTagArray>;
export const RuleTagArray = z.array(z.string());
export type RuleMetadata = z.infer<typeof RuleMetadata>;
export const RuleMetadata = z.object({});
export const RuleMetadata = z.object({}).catchall(z.unknown());
/**
* The rule's license.
*/
export type RuleLicense = z.infer<typeof RuleLicense>;
export const RuleLicense = z.string();
@ -60,26 +149,38 @@ export const RuleFalsePositiveArray = z.array(z.string());
export type RuleReferenceArray = z.infer<typeof RuleReferenceArray>;
export const RuleReferenceArray = z.array(z.string());
/**
* Notes to help investigate alerts produced by the rule.
*/
export type InvestigationGuide = z.infer<typeof InvestigationGuide>;
export const InvestigationGuide = z.string();
export type SetupGuide = z.infer<typeof SetupGuide>;
export const SetupGuide = z.string();
/**
* Determines if the rule acts as a building block. By default, building-block alerts are not displayed in the UI. These rules are used as a foundation for other rules that do generate alerts. Its value must be default.
*/
export type BuildingBlockType = z.infer<typeof BuildingBlockType>;
export const BuildingBlockType = z.string();
/**
* (deprecated) Has no effect.
*/
export type AlertsIndex = z.infer<typeof AlertsIndex>;
export const AlertsIndex = z.string();
/**
* Has no effect.
*/
export type AlertsIndexNamespace = z.infer<typeof AlertsIndexNamespace>;
export const AlertsIndexNamespace = z.string();
export type MaxSignals = z.infer<typeof MaxSignals>;
export const MaxSignals = z.number().int().min(1);
export type Subtechnique = z.infer<typeof Subtechnique>;
export const Subtechnique = z.object({
export type ThreatSubtechnique = z.infer<typeof ThreatSubtechnique>;
export const ThreatSubtechnique = z.object({
/**
* Subtechnique ID
*/
@ -94,8 +195,8 @@ export const Subtechnique = z.object({
reference: z.string(),
});
export type Technique = z.infer<typeof Technique>;
export const Technique = z.object({
export type ThreatTechnique = z.infer<typeof ThreatTechnique>;
export const ThreatTechnique = z.object({
/**
* Technique ID
*/
@ -111,7 +212,23 @@ export const Technique = z.object({
/**
* Array containing more specific information on the attack technique
*/
subtechnique: z.array(Subtechnique).optional(),
subtechnique: z.array(ThreatSubtechnique).optional(),
});
export type ThreatTactic = z.infer<typeof ThreatTactic>;
export const ThreatTactic = z.object({
/**
* Tactic ID
*/
id: z.string(),
/**
* Tactic name
*/
name: z.string(),
/**
* Tactic reference
*/
reference: z.string(),
});
export type Threat = z.infer<typeof Threat>;
@ -120,24 +237,11 @@ export const Threat = z.object({
* Relevant attack framework
*/
framework: z.string(),
tactic: z.object({
/**
* Tactic ID
*/
id: z.string(),
/**
* Tactic name
*/
name: z.string(),
/**
* Tactic reference
*/
reference: z.string(),
}),
tactic: ThreatTactic,
/**
* Array containing information on the attack techniques (optional)
*/
technique: z.array(Technique).optional(),
technique: z.array(ThreatTechnique).optional(),
});
export type ThreatArray = z.infer<typeof ThreatArray>;
@ -149,41 +253,59 @@ export const IndexPatternArray = z.array(z.string());
export type DataViewId = z.infer<typeof DataViewId>;
export const DataViewId = z.string();
export type SavedQueryId = z.infer<typeof SavedQueryId>;
export const SavedQueryId = z.string();
export type RuleQuery = z.infer<typeof RuleQuery>;
export const RuleQuery = z.string();
export type RuleFilterArray = z.infer<typeof RuleFilterArray>;
export const RuleFilterArray = z.array(z.object({}));
export const RuleFilterArray = z.array(z.unknown());
/**
* Sets the source field for the alert's signal.rule.name value
*/
export type RuleNameOverride = z.infer<typeof RuleNameOverride>;
export const RuleNameOverride = z.string();
/**
* Sets the time field used to query indices
*/
export type TimestampOverride = z.infer<typeof TimestampOverride>;
export const TimestampOverride = z.string();
/**
* Disables the fallback to the event's @timestamp field
*/
export type TimestampOverrideFallbackDisabled = z.infer<typeof TimestampOverrideFallbackDisabled>;
export const TimestampOverrideFallbackDisabled = z.boolean();
export type RequiredField = z.infer<typeof RequiredField>;
export const RequiredField = z.object({
name: z.string().min(1).optional(),
type: z.string().min(1).optional(),
ecs: z.boolean().optional(),
name: NonEmptyString,
type: NonEmptyString,
ecs: z.boolean(),
});
export type RequiredFieldArray = z.infer<typeof RequiredFieldArray>;
export const RequiredFieldArray = z.array(RequiredField);
/**
* Timeline template ID
*/
export type TimelineTemplateId = z.infer<typeof TimelineTemplateId>;
export const TimelineTemplateId = z.string();
/**
* Timeline template title
*/
export type TimelineTemplateTitle = z.infer<typeof TimelineTemplateTitle>;
export const TimelineTemplateTitle = z.string();
export type SavedObjectResolveOutcome = z.infer<typeof SavedObjectResolveOutcome>;
export const SavedObjectResolveOutcome = z.enum(['exactMatch', 'aliasMatch', 'conflict']);
export const SavedObjectResolveOutcomeEnum = SavedObjectResolveOutcome.enum;
export type SavedObjectResolveOutcomeEnum = typeof SavedObjectResolveOutcome.enum;
export const SavedObjectResolveOutcomeEnum = SavedObjectResolveOutcome.enum;
export type SavedObjectResolveAliasTargetId = z.infer<typeof SavedObjectResolveAliasTargetId>;
export const SavedObjectResolveAliasTargetId = z.string();
@ -193,15 +315,110 @@ export const SavedObjectResolveAliasPurpose = z.enum([
'savedObjectConversion',
'savedObjectImport',
]);
export const SavedObjectResolveAliasPurposeEnum = SavedObjectResolveAliasPurpose.enum;
export type SavedObjectResolveAliasPurposeEnum = typeof SavedObjectResolveAliasPurpose.enum;
export const SavedObjectResolveAliasPurposeEnum = SavedObjectResolveAliasPurpose.enum;
export type RelatedIntegration = z.infer<typeof RelatedIntegration>;
export const RelatedIntegration = z.object({
package: z.string().min(1),
version: z.string().min(1),
integration: z.string().min(1).optional(),
package: NonEmptyString,
version: NonEmptyString,
integration: NonEmptyString.optional(),
});
export type RelatedIntegrationArray = z.infer<typeof RelatedIntegrationArray>;
export const RelatedIntegrationArray = z.array(RelatedIntegration);
export type InvestigationFields = z.infer<typeof InvestigationFields>;
export const InvestigationFields = z.object({
field_names: z.array(NonEmptyString).min(1),
});
/**
* Defines the interval on which a rule's actions are executed.
*/
export type RuleActionThrottle = z.infer<typeof RuleActionThrottle>;
export const RuleActionThrottle = z.union([
z.enum(['no_actions', 'rule']),
z.string().regex(/^[1-9]\d*[smhd]$/),
]);
/**
* The condition for throttling the notification: `onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval`
*/
export type RuleActionNotifyWhen = z.infer<typeof RuleActionNotifyWhen>;
export const RuleActionNotifyWhen = z.enum([
'onActiveAlert',
'onThrottleInterval',
'onActionGroupChange',
]);
export type RuleActionNotifyWhenEnum = typeof RuleActionNotifyWhen.enum;
export const RuleActionNotifyWhenEnum = RuleActionNotifyWhen.enum;
/**
* The action frequency defines when the action runs (for example, only on rule execution or at specific time intervals).
*/
export type RuleActionFrequency = z.infer<typeof RuleActionFrequency>;
export const RuleActionFrequency = z.object({
/**
* Action summary indicates whether we will send a summary notification about all the generate alerts or notification per individual alert
*/
summary: z.boolean(),
notifyWhen: RuleActionNotifyWhen,
throttle: RuleActionThrottle.nullable(),
});
export type RuleAction = z.infer<typeof RuleAction>;
export const RuleAction = z.object({
/**
* The action type used for sending notifications.
*/
action_type_id: z.string(),
/**
* Optionally groups actions by use cases. Use `default` for alert notifications.
*/
group: z.string(),
/**
* The connector ID.
*/
id: z.string(),
/**
* Object containing the allowed connector fields, which varies according to the connector type.
*/
params: z.object({}).catchall(z.unknown()),
uuid: NonEmptyString.optional(),
alerts_filter: z.object({}).catchall(z.unknown()).optional(),
frequency: RuleActionFrequency.optional(),
});
/**
* The exception type
*/
export type ExceptionListType = z.infer<typeof ExceptionListType>;
export const ExceptionListType = z.enum([
'detection',
'rule_default',
'endpoint',
'endpoint_trusted_apps',
'endpoint_events',
'endpoint_host_isolation_exceptions',
'endpoint_blocklists',
]);
export type ExceptionListTypeEnum = typeof ExceptionListType.enum;
export const ExceptionListTypeEnum = ExceptionListType.enum;
export type RuleExceptionList = z.infer<typeof RuleExceptionList>;
export const RuleExceptionList = z.object({
/**
* ID of the exception container
*/
id: NonEmptyString,
/**
* List ID of the exception container
*/
list_id: NonEmptyString,
type: ExceptionListType,
/**
* Determines the exceptions validity in rule's Kibana space
*/
namespace_type: z.enum(['agnostic', 'single']),
});

View file

@ -6,13 +6,19 @@ paths: {}
components:
x-codegen-enabled: true
schemas:
NonEmptyString:
type: string
pattern: ^(?! *$).+$
minLength: 1
description: A string that is not empty and does not contain only whitespace
UUID:
type: string
format: uuid
description: A universally unique identifier
RuleObjectId:
type: string
$ref: '#/components/schemas/UUID'
RuleSignatureId:
type: string
@ -27,19 +33,103 @@ components:
minLength: 1
RuleVersion:
type: integer
minimum: 1
description: The rule's version number.
QueryLanguage:
type: string
format: version
enum:
- kuery
- lucene
- eql
- esql
KqlQueryLanguage:
type: string
enum:
- kuery
- lucene
IsRuleImmutable:
type: boolean
IsRuleEnabled:
type: boolean
description: Determines whether the rule is enabled.
RuleInterval:
type: string
description: Frequency of rule execution, using a date math range. For example, "1h" means the rule runs every hour. Defaults to 5m (5 minutes).
RuleIntervalFrom:
type: string
description: Time from which data is analyzed each time the rule executes, using a date math range. For example, now-4200s means the rule analyzes data from 70 minutes before its start time. Defaults to now-6m (analyzes data from 6 minutes before the start time).
format: date-math
RuleIntervalTo:
type: string
RiskScore:
type: integer
description: Risk score (0 to 100)
minimum: 0
maximum: 100
RiskScoreMapping:
type: array
items:
type: object
properties:
field:
type: string
operator:
type: string
enum:
- equals
value:
type: string
risk_score:
$ref: '#/components/schemas/RiskScore'
required:
- field
- operator
- value
x-modify: requiredOptional
description: Overrides generated alerts' risk_score with a value from the source event
Severity:
type: string
enum: [low, medium, high, critical]
description: Severity of the rule
SeverityMapping:
type: array
items:
type: object
properties:
field:
type: string
operator:
type: string
enum:
- equals
severity:
$ref: '#/components/schemas/Severity'
value:
type: string
required:
- field
- operator
- severity
- value
description: Overrides generated alerts' severity with values from the source event
RuleTagArray:
type: array
items:
type: string
description: String array containing words and phrases to help categorize, filter, and search rules. Defaults to an empty array.
RuleMetadata:
type: object
@ -47,6 +137,7 @@ components:
RuleLicense:
type: string
description: The rule's license.
RuleAuthorArray:
type: array
@ -65,24 +156,29 @@ components:
InvestigationGuide:
type: string
description: Notes to help investigate alerts produced by the rule.
SetupGuide:
type: string
BuildingBlockType:
type: string
description: Determines if the rule acts as a building block. By default, building-block alerts are not displayed in the UI. These rules are used as a foundation for other rules that do generate alerts. Its value must be default.
AlertsIndex:
type: string
description: (deprecated) Has no effect.
deprecated: true
AlertsIndexNamespace:
type: string
description: Has no effect.
MaxSignals:
type: integer
minimum: 1
Subtechnique:
ThreatSubtechnique:
type: object
properties:
id:
@ -99,7 +195,7 @@ components:
- name
- reference
Technique:
ThreatTechnique:
type: object
properties:
id:
@ -114,13 +210,30 @@ components:
subtechnique:
type: array
items:
$ref: '#/components/schemas/Subtechnique'
$ref: '#/components/schemas/ThreatSubtechnique'
description: Array containing more specific information on the attack technique
required:
- id
- name
- reference
ThreatTactic:
type: object
properties:
id:
type: string
description: Tactic ID
name:
type: string
description: Tactic name
reference:
type: string
description: Tactic reference
required:
- id
- name
- reference
Threat:
type: object
properties:
@ -128,25 +241,11 @@ components:
type: string
description: Relevant attack framework
tactic:
type: object
properties:
id:
type: string
description: Tactic ID
name:
type: string
description: Tactic name
reference:
type: string
description: Tactic reference
required:
- id
- name
- reference
$ref: '#/components/schemas/ThreatTactic'
technique:
type: array
items:
$ref: '#/components/schemas/Technique'
$ref: '#/components/schemas/ThreatTechnique'
description: Array containing information on the attack techniques (optional)
required:
- framework
@ -155,7 +254,7 @@ components:
ThreatArray:
type: array
items:
$ref: '#/components/schemas/Threat' # Assuming a schema named 'Threat' is defined in the components section.
$ref: '#/components/schemas/Threat'
IndexPatternArray:
type: array
@ -165,35 +264,41 @@ components:
DataViewId:
type: string
SavedQueryId:
type: string
RuleQuery:
type: string
RuleFilterArray:
type: array
items:
type: object
additionalProperties: true
items: {} # unknown
RuleNameOverride:
type: string
description: Sets the source field for the alert's signal.rule.name value
TimestampOverride:
type: string
description: Sets the time field used to query indices
TimestampOverrideFallbackDisabled:
type: boolean
description: Disables the fallback to the event's @timestamp field
RequiredField:
type: object
properties:
name:
type: string
minLength: 1
$ref: '#/components/schemas/NonEmptyString'
type:
type: string
minLength: 1
$ref: '#/components/schemas/NonEmptyString'
ecs:
type: boolean
required:
- name
- type
- ecs
RequiredFieldArray:
type: array
@ -202,9 +307,11 @@ components:
TimelineTemplateId:
type: string
description: Timeline template ID
TimelineTemplateTitle:
type: string
description: Timeline template title
SavedObjectResolveOutcome:
type: string
@ -226,14 +333,11 @@ components:
type: object
properties:
package:
type: string
minLength: 1
$ref: '#/components/schemas/NonEmptyString'
version:
type: string
minLength: 1
$ref: '#/components/schemas/NonEmptyString'
integration:
type: string
minLength: 1
$ref: '#/components/schemas/NonEmptyString'
required:
- package
- version
@ -242,3 +346,117 @@ components:
type: array
items:
$ref: '#/components/schemas/RelatedIntegration'
# Schema for fields relating to investigation fields, these are user defined fields we use to highlight in various features in the UI such as alert details flyout and exceptions auto-population from alert. Added in PR #163235
# Right now we only have a single field but anticipate adding more related fields to store various configuration states such as `override` - where a user might say if they want only these fields to display, or if they want these fields + the fields we select.
InvestigationFields:
type: object
properties:
field_names:
type: array
items:
$ref: '#/components/schemas/NonEmptyString'
minItems: 1
required:
- field_names
RuleActionThrottle:
description: Defines the interval on which a rule's actions are executed.
oneOf:
- type: string
enum:
- 'no_actions'
- 'rule'
- type: string
pattern: '^[1-9]\d*[smhd]$' # any number except zero followed by one of the suffixes 's', 'm', 'h', 'd'
description: Time interval in seconds, minutes, hours, or days.
example: '1h'
RuleActionNotifyWhen:
type: string
enum:
- 'onActiveAlert'
- 'onThrottleInterval'
- 'onActionGroupChange'
description: 'The condition for throttling the notification: `onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval`'
RuleActionFrequency:
type: object
description: The action frequency defines when the action runs (for example, only on rule execution or at specific time intervals).
properties:
summary:
type: boolean
description: Action summary indicates whether we will send a summary notification about all the generate alerts or notification per individual alert
notifyWhen:
$ref: '#/components/schemas/RuleActionNotifyWhen'
throttle:
$ref: '#/components/schemas/RuleActionThrottle'
nullable: true
required:
- summary
- notifyWhen
- throttle
RuleAction:
type: object
properties:
action_type_id:
type: string
description: The action type used for sending notifications.
group:
type: string
description: Optionally groups actions by use cases. Use `default` for alert notifications.
id:
type: string
description: The connector ID.
params:
type: object
description: Object containing the allowed connector fields, which varies according to the connector type.
additionalProperties: true
uuid:
$ref: '#/components/schemas/NonEmptyString'
alerts_filter:
type: object
additionalProperties: true
frequency:
$ref: '#/components/schemas/RuleActionFrequency'
required:
- action_type_id
- group
- id
- params
ExceptionListType:
type: string
description: The exception type
enum:
- detection
- rule_default
- endpoint
- endpoint_trusted_apps
- endpoint_events
- endpoint_host_isolation_exceptions
- endpoint_blocklists
RuleExceptionList:
type: object
properties:
id:
$ref: '#/components/schemas/NonEmptyString'
description: ID of the exception container
list_id:
$ref: '#/components/schemas/NonEmptyString'
description: List ID of the exception container
type:
$ref: '#/components/schemas/ExceptionListType'
namespace_type:
type: string
description: Determines the exceptions validity in rule's Kibana space
enum:
- agnostic
- single
required:
- id
- list_id
- type
- namespace_type

View file

@ -5,12 +5,12 @@
* 2.0.
*/
export * from './common_attributes';
export * from './common_attributes.gen';
export * from './rule_schemas.gen';
export * from './specific_attributes/eql_attributes';
export * from './specific_attributes/new_terms_attributes';
export * from './specific_attributes/query_attributes';
export * from './specific_attributes/threshold_attributes';
export * from './rule_schemas';
export * from './build_rule_schemas';
export * from './specific_attributes/eql_attributes.gen';
export * from './specific_attributes/ml_attributes.gen';
export * from './specific_attributes/new_terms_attributes.gen';
export * from './specific_attributes/query_attributes.gen';
export * from './specific_attributes/threat_match_attributes.gen';
export * from './specific_attributes/threshold_attributes.gen';

View file

@ -17,7 +17,7 @@ import type {
ThresholdRuleCreateProps,
NewTermsRuleCreateProps,
NewTermsRuleUpdateProps,
} from './rule_schemas';
} from './rule_schemas.gen';
export const getCreateRulesSchemaMock = (ruleId = 'rule-1'): QueryRuleCreateProps => ({
description: 'Detecting root and admin users',

View file

@ -14,7 +14,7 @@ import type {
SavedQueryRule,
SharedResponseProps,
ThreatMatchRule,
} from './rule_schemas';
} from './rule_schemas.gen';
import { getListArrayMock } from '../../../../detection_engine/schemas/types/lists.mock';
export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z';

View file

@ -4,12 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { left } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
import { RuleResponse } from './rule_schemas';
import { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import { RuleResponse } from './rule_schemas.gen';
import {
getRulesSchemaMock,
getRulesMlSchemaMock,
@ -23,37 +19,28 @@ describe('Rule response schema', () => {
test('it should validate a type of "query" without anything extra', () => {
const payload = getRulesSchemaMock();
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
const expected = getRulesSchemaMock();
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(expected);
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should NOT validate a type of "query" when it has extra data', () => {
test('it should strip any extra data', () => {
const payload: RuleResponse & { invalid_extra_data?: string } = getRulesSchemaMock();
payload.invalid_extra_data = 'invalid_extra_data';
const expected = getRulesSchemaMock();
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']);
expect(message.schema).toEqual({});
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(expected);
});
test('it should NOT validate invalid_data for the type', () => {
const payload: Omit<RuleResponse, 'type'> & { type: string } = getRulesSchemaMock();
payload.type = 'invalid_data';
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toHaveLength(1);
expect(message.schema).toEqual({});
const result = RuleResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
});
test('it should validate a type of "query" with a saved_id together', () => {
@ -61,24 +48,17 @@ describe('Rule response schema', () => {
payload.type = 'query';
payload.saved_id = 'save id 123';
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should validate a type of "saved_query" with a "saved_id" dependent', () => {
const payload = getSavedQuerySchemaMock();
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
const expected = getSavedQuerySchemaMock();
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(expected);
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should NOT validate a type of "saved_query" without a "saved_id" dependent', () => {
@ -86,27 +66,9 @@ describe('Rule response schema', () => {
// @ts-expect-error
delete payload.saved_id;
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "saved_id"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate a type of "saved_query" when it has extra data', () => {
const payload: RuleResponse & { saved_id?: string; invalid_extra_data?: string } =
getSavedQuerySchemaMock();
payload.invalid_extra_data = 'invalid_extra_data';
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']);
expect(message.schema).toEqual({});
const result = RuleResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
});
test('it should validate a type of "timeline_id" if there is a "timeline_title" dependent', () => {
@ -114,42 +76,19 @@ describe('Rule response schema', () => {
payload.timeline_id = 'some timeline id';
payload.timeline_title = 'some timeline title';
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
const expected = getRulesSchemaMock();
expected.timeline_id = 'some timeline id';
expected.timeline_title = 'some timeline title';
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(expected);
});
test('it should NOT validate a type of "timeline_id" if there is "timeline_title" dependent when it has extra invalid data', () => {
const payload: RuleResponse & { invalid_extra_data?: string } = getRulesSchemaMock();
payload.timeline_id = 'some timeline id';
payload.timeline_title = 'some timeline title';
payload.invalid_extra_data = 'invalid_extra_data';
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']);
expect(message.schema).toEqual({});
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
describe('exceptions_list', () => {
test('it should validate an empty array for "exceptions_list"', () => {
const payload = getRulesSchemaMock();
payload.exceptions_list = [];
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
const expected = getRulesSchemaMock();
expected.exceptions_list = [];
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(expected);
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should NOT validate when "exceptions_list" is not expected type', () => {
@ -157,49 +96,38 @@ describe('Rule response schema', () => {
exceptions_list?: string;
} = { ...getRulesSchemaMock(), exceptions_list: 'invalid_data' };
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "invalid_data" supplied to "exceptions_list"',
]);
expect(message.schema).toEqual({});
const result = RuleResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
});
});
describe('esql rule type', () => {
test('it should NOT validate a type of "esql" with "index" defined', () => {
test('it should omit the "index" field', () => {
const payload = { ...getEsqlRuleSchemaMock(), index: ['logs-*'] };
const expected = getEsqlRuleSchemaMock();
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "index,["logs-*"]"']);
expect(message.schema).toEqual({});
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(expected);
});
test('it should NOT validate a type of "esql" with "filters" defined', () => {
test('it should omit the "filters" field', () => {
const payload = { ...getEsqlRuleSchemaMock(), filters: [] };
const expected = getEsqlRuleSchemaMock();
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "filters,[]"']);
expect(message.schema).toEqual({});
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(expected);
});
test('it should NOT validate a type of "esql" with a "saved_id" dependent', () => {
test('it should omit the "saved_id" field', () => {
const payload = { ...getEsqlRuleSchemaMock(), saved_id: 'id' };
const expected = getEsqlRuleSchemaMock();
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "saved_id"']);
expect(message.schema).toEqual({});
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(expected);
});
});
@ -207,13 +135,9 @@ describe('Rule response schema', () => {
test('it should validate a type of "query" with "data_view_id" defined', () => {
const payload = { ...getRulesSchemaMock(), data_view_id: 'logs-*' };
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
const expected = { ...getRulesSchemaMock(), data_view_id: 'logs-*' };
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(expected);
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should validate a type of "saved_query" with "data_view_id" defined', () => {
@ -221,149 +145,93 @@ describe('Rule response schema', () => {
getSavedQuerySchemaMock();
payload.data_view_id = 'logs-*';
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
const expected: RuleResponse & { saved_id?: string; data_view_id?: string } =
getSavedQuerySchemaMock();
expected.data_view_id = 'logs-*';
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(expected);
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should validate a type of "eql" with "data_view_id" defined', () => {
const payload = { ...getRulesEqlSchemaMock(), data_view_id: 'logs-*' };
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
const expected = { ...getRulesEqlSchemaMock(), data_view_id: 'logs-*' };
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(expected);
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should validate a type of "threat_match" with "data_view_id" defined', () => {
const payload = { ...getThreatMatchingSchemaMock(), data_view_id: 'logs-*' };
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
const expected = { ...getThreatMatchingSchemaMock(), data_view_id: 'logs-*' };
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(expected);
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should NOT validate a type of "machine_learning" with "data_view_id" defined', () => {
test('it should omit the "data_view_id" field for "machine_learning"rules', () => {
const payload = { ...getRulesMlSchemaMock(), data_view_id: 'logs-*' };
const expected = getRulesMlSchemaMock();
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "data_view_id"']);
expect(message.schema).toEqual({});
});
test('it should NOT validate a type of "esql" with "data_view_id" defined', () => {
const payload = { ...getEsqlRuleSchemaMock(), data_view_id: 'logs-*' };
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "data_view_id"']);
expect(message.schema).toEqual({});
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(expected);
});
});
describe('investigation_fields', () => {
test('it should validate rule with "investigation_fields"', () => {
const payload = getRulesSchemaMock();
payload.investigation_fields = { field_names: ['foo', 'bar'] };
test('it should omit the "data_view_id" field for "esql" rules', () => {
const payload = { ...getEsqlRuleSchemaMock(), data_view_id: 'logs-*' };
const expected = getEsqlRuleSchemaMock();
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
const expected = {
...getRulesSchemaMock(),
investigation_fields: { field_names: ['foo', 'bar'] },
};
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(expected);
});
test('it should validate undefined for "investigation_fields"', () => {
const payload: RuleResponse = {
...getRulesSchemaMock(),
investigation_fields: undefined,
};
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
const expected = { ...getRulesSchemaMock(), investigation_fields: undefined };
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(expected);
});
test('it should validate "investigation_fields" not in schema', () => {
const payload: RuleResponse = {
...getRulesSchemaMock(),
investigation_fields: undefined,
};
delete payload.investigation_fields;
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
const expected = getRulesSchemaMock();
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(expected);
});
test('it should NOT validate an empty array for "investigation_fields.field_names"', () => {
const payload: RuleResponse = {
...getRulesSchemaMock(),
investigation_fields: {
field_names: [],
},
};
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "[]" supplied to "investigation_fields,field_names"',
'Invalid value "{"field_names":[]}" supplied to "investigation_fields"',
]);
expect(message.schema).toEqual({});
});
test('it should NOT validate a string for "investigation_fields"', () => {
const payload: Omit<RuleResponse, 'investigation_fields'> & {
investigation_fields: string;
} = {
...getRulesSchemaMock(),
investigation_fields: 'foo',
};
const decoded = RuleResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "foo" supplied to "investigation_fields"',
]);
expect(message.schema).toEqual({});
});
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(expected);
});
});
describe('investigation_fields', () => {
test('it should validate rule with "investigation_fields"', () => {
const payload = getRulesSchemaMock();
payload.investigation_fields = { field_names: ['foo', 'bar'] };
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should validate undefined for "investigation_fields"', () => {
const payload: RuleResponse = {
...getRulesSchemaMock(),
investigation_fields: undefined,
};
const result = RuleResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should NOT validate an empty array for "investigation_fields.field_names"', () => {
const payload: RuleResponse = {
...getRulesSchemaMock(),
investigation_fields: {
field_names: [],
},
};
const result = RuleResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual(
'investigation_fields.field_names: Array must contain at least 1 element(s)'
);
});
test('it should NOT validate a string for "investigation_fields"', () => {
const payload: Omit<RuleResponse, 'investigation_fields'> & {
investigation_fields: string;
} = {
...getRulesSchemaMock(),
investigation_fields: 'foo',
};
const result = RuleResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
});
});

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from 'zod';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
export type EventCategoryOverride = z.infer<typeof EventCategoryOverride>;
export const EventCategoryOverride = z.string();
/**
* Contains the event timestamp used for sorting a sequence of events
*/
export type TimestampField = z.infer<typeof TimestampField>;
export const TimestampField = z.string();
/**
* Sets a secondary field for sorting events
*/
export type TiebreakerField = z.infer<typeof TiebreakerField>;
export const TiebreakerField = z.string();

View file

@ -0,0 +1,16 @@
openapi: 3.0.0
info:
title: EQL Rule Attributes
version: 'not applicable'
paths: {}
components:
x-codegen-enabled: true
schemas:
EventCategoryOverride:
type: string
TimestampField:
type: string
description: Contains the event timestamp used for sorting a sequence of events
TiebreakerField:
type: string
description: Sets a secondary field for sorting events

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from 'zod';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
/**
* Anomaly threshold
*/
export type AnomalyThreshold = z.infer<typeof AnomalyThreshold>;
export const AnomalyThreshold = z.number().int().min(0);
/**
* Machine learning job ID
*/
export type MachineLearningJobId = z.infer<typeof MachineLearningJobId>;
export const MachineLearningJobId = z.union([z.string(), z.array(z.string()).min(1)]);

View file

@ -0,0 +1,20 @@
openapi: 3.0.0
info:
title: ML Rule Attributes
version: 'not applicable'
paths: {}
components:
x-codegen-enabled: true
schemas:
AnomalyThreshold:
type: integer
minimum: 0
description: Anomaly threshold
MachineLearningJobId:
oneOf:
- type: string
- type: array
items:
type: string
minItems: 1
description: Machine learning job ID

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from 'zod';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
import { NonEmptyString } from '../common_attributes.gen';
export type NewTermsFields = z.infer<typeof NewTermsFields>;
export const NewTermsFields = z.array(z.string()).min(1).max(3);
export type HistoryWindowStart = z.infer<typeof HistoryWindowStart>;
export const HistoryWindowStart = NonEmptyString;

View file

@ -0,0 +1,16 @@
openapi: 3.0.0
info:
title: New Terms Attributes
version: 'not applicable'
paths: {}
components:
x-codegen-enabled: true
schemas:
NewTermsFields:
type: array
items:
type: string
minItems: 1
maxItems: 3
HistoryWindowStart:
$ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString'

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from 'zod';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
/**
* Describes how alerts will be generated for documents with missing suppress by fields:
doNotSuppress - per each document a separate alert will be created
suppress - only alert will be created per suppress by bucket
*/
export type AlertSuppressionMissingFieldsStrategy = z.infer<
typeof AlertSuppressionMissingFieldsStrategy
>;
export const AlertSuppressionMissingFieldsStrategy = z.enum(['doNotSuppress', 'suppress']);
export type AlertSuppressionMissingFieldsStrategyEnum =
typeof AlertSuppressionMissingFieldsStrategy.enum;
export const AlertSuppressionMissingFieldsStrategyEnum = AlertSuppressionMissingFieldsStrategy.enum;
export type AlertSuppressionGroupBy = z.infer<typeof AlertSuppressionGroupBy>;
export const AlertSuppressionGroupBy = z.array(z.string()).min(1).max(3);
export type AlertSuppressionDuration = z.infer<typeof AlertSuppressionDuration>;
export const AlertSuppressionDuration = z.object({
value: z.number().int().min(1),
unit: z.enum(['s', 'm', 'h']),
});
export type AlertSuppression = z.infer<typeof AlertSuppression>;
export const AlertSuppression = z.object({
group_by: AlertSuppressionGroupBy,
duration: AlertSuppressionDuration.optional(),
missing_fields_strategy: AlertSuppressionMissingFieldsStrategy.optional(),
});
export type AlertSuppressionCamel = z.infer<typeof AlertSuppressionCamel>;
export const AlertSuppressionCamel = z.object({
groupBy: AlertSuppressionGroupBy,
duration: AlertSuppressionDuration.optional(),
missingFieldsStrategy: AlertSuppressionMissingFieldsStrategy.optional(),
});

View file

@ -0,0 +1,64 @@
openapi: 3.0.0
info:
title: Query Rule Attributes
version: 'not applicable'
paths: {}
components:
x-codegen-enabled: true
schemas:
AlertSuppressionMissingFieldsStrategy:
type: string
enum:
- doNotSuppress
- suppress
description: |-
Describes how alerts will be generated for documents with missing suppress by fields:
doNotSuppress - per each document a separate alert will be created
suppress - only alert will be created per suppress by bucket
AlertSuppressionGroupBy:
type: array
items:
type: string
minItems: 1
maxItems: 3
AlertSuppressionDuration:
type: object
properties:
value:
type: integer
minimum: 1
unit:
type: string
enum:
- s
- m
- h
required:
- value
- unit
AlertSuppression:
type: object
properties:
group_by:
$ref: '#/components/schemas/AlertSuppressionGroupBy'
duration:
$ref: '#/components/schemas/AlertSuppressionDuration'
missing_fields_strategy:
$ref: '#/components/schemas/AlertSuppressionMissingFieldsStrategy'
required:
- group_by
AlertSuppressionCamel:
type: object
properties:
groupBy:
$ref: '#/components/schemas/AlertSuppressionGroupBy'
duration:
$ref: '#/components/schemas/AlertSuppressionDuration'
missingFieldsStrategy:
$ref: '#/components/schemas/AlertSuppressionMissingFieldsStrategy'
required:
- groupBy

View file

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from 'zod';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
import { NonEmptyString } from '../common_attributes.gen';
/**
* Query to execute
*/
export type ThreatQuery = z.infer<typeof ThreatQuery>;
export const ThreatQuery = z.string();
export type ThreatMapping = z.infer<typeof ThreatMapping>;
export const ThreatMapping = z
.array(
z.object({
entries: z.array(
z.object({
field: NonEmptyString,
type: z.literal('mapping'),
value: NonEmptyString,
})
),
})
)
.min(1);
export type ThreatIndex = z.infer<typeof ThreatIndex>;
export const ThreatIndex = z.array(z.string());
export type ThreatFilters = z.infer<typeof ThreatFilters>;
export const ThreatFilters = z.array(z.unknown());
/**
* Defines the path to the threat indicator in the indicator documents (optional)
*/
export type ThreatIndicatorPath = z.infer<typeof ThreatIndicatorPath>;
export const ThreatIndicatorPath = z.string();
export type ConcurrentSearches = z.infer<typeof ConcurrentSearches>;
export const ConcurrentSearches = z.number().int().min(1);
export type ItemsPerSearch = z.infer<typeof ItemsPerSearch>;
export const ItemsPerSearch = z.number().int().min(1);

View file

@ -0,0 +1,59 @@
openapi: 3.0.0
info:
title: Threat Match Rule Attributes
version: 'not applicable'
paths: {}
components:
x-codegen-enabled: true
schemas:
ThreatQuery:
type: string
description: Query to execute
ThreatMapping:
type: array
minItems: 1
items:
type: object
properties:
entries:
type: array
items:
type: object
properties:
field:
$ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString'
type:
type: string
enum:
- mapping
value:
$ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString'
required:
- field
- type
- value
required:
- entries
ThreatIndex:
type: array
items:
type: string
ThreatFilters:
type: array
items:
description: Query and filter context array used to filter documents from the Elasticsearch index containing the threat values
ThreatIndicatorPath:
type: string
description: Defines the path to the threat indicator in the indicator documents (optional)
ConcurrentSearches:
type: integer
minimum: 1
ItemsPerSearch:
type: integer
minimum: 1

View file

@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from 'zod';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
export type ThresholdCardinality = z.infer<typeof ThresholdCardinality>;
export const ThresholdCardinality = z.array(
z.object({
field: z.string(),
value: z.number().int().min(0),
})
);
/**
* Threshold value
*/
export type ThresholdValue = z.infer<typeof ThresholdValue>;
export const ThresholdValue = z.number().int().min(1);
/**
* Field to aggregate on
*/
export type ThresholdField = z.infer<typeof ThresholdField>;
export const ThresholdField = z.union([z.string(), z.array(z.string())]);
/**
* Field to aggregate on
*/
export type ThresholdFieldNormalized = z.infer<typeof ThresholdFieldNormalized>;
export const ThresholdFieldNormalized = z.array(z.string());
export type Threshold = z.infer<typeof Threshold>;
export const Threshold = z.object({
field: ThresholdField,
value: ThresholdValue,
cardinality: ThresholdCardinality.optional(),
});
export type ThresholdNormalized = z.infer<typeof ThresholdNormalized>;
export const ThresholdNormalized = z.object({
field: ThresholdFieldNormalized,
value: ThresholdValue,
cardinality: ThresholdCardinality.optional(),
});
export type ThresholdWithCardinality = z.infer<typeof ThresholdWithCardinality>;
export const ThresholdWithCardinality = z.object({
field: ThresholdFieldNormalized,
value: ThresholdValue,
cardinality: ThresholdCardinality,
});

View file

@ -0,0 +1,80 @@
openapi: 3.0.0
info:
title: Threshold Rule Attributes
version: 'not applicable'
paths: {}
components:
x-codegen-enabled: true
schemas:
ThresholdCardinality:
type: array
items:
type: object
properties:
field:
type: string
value:
type: integer
minimum: 0
required:
- field
- value
ThresholdValue:
type: integer
minimum: 1
description: Threshold value
ThresholdField:
oneOf:
- type: string
- type: array
items:
type: string
description: Field to aggregate on
ThresholdFieldNormalized:
type: array
items:
type: string
description: Field to aggregate on
Threshold:
type: object
properties:
field:
$ref: '#/components/schemas/ThresholdField'
value:
$ref: '#/components/schemas/ThresholdValue'
cardinality:
$ref: '#/components/schemas/ThresholdCardinality'
required:
- field
- value
ThresholdNormalized:
type: object
properties:
field:
$ref: '#/components/schemas/ThresholdFieldNormalized'
value:
$ref: '#/components/schemas/ThresholdValue'
cardinality:
$ref: '#/components/schemas/ThresholdCardinality'
required:
- field
- value
ThresholdWithCardinality:
type: object
properties:
field:
$ref: '#/components/schemas/ThresholdFieldNormalized'
value:
$ref: '#/components/schemas/ThresholdValue'
cardinality:
$ref: '#/components/schemas/ThresholdCardinality'
required:
- field
- value
- cardinality

View file

@ -44,7 +44,7 @@ export const RuleTagArray = t.array(t.string); // should be non-empty strings?
* to be added to the meta object
*/
export type RuleMetadata = t.TypeOf<typeof RuleMetadata>;
export const RuleMetadata = t.object; // should be a more specific type?
export const RuleMetadata = t.UnknownRecord; // should be a more specific type?
export type RuleLicense = t.TypeOf<typeof RuleLicense>;
export const RuleLicense = t.string;

View file

@ -5,9 +5,11 @@
* 2.0.
*/
import * as t from 'io-ts';
export * from './common_attributes';
import { RuleResponse, ErrorSchema } from '../../model';
export * from './eql_attributes';
export * from './new_terms_attributes';
export * from './query_attributes';
export * from './threshold_attributes';
export type BulkCrudRulesResponse = t.TypeOf<typeof BulkCrudRulesResponse>;
export const BulkCrudRulesResponse = t.array(t.union([RuleResponse, ErrorSchema]));
export * from './rule_schemas';

View file

@ -7,7 +7,7 @@
import * as t from 'io-ts';
import { LimitedSizeArray, NonEmptyString } from '@kbn/securitysolution-io-ts-types';
import { MAX_NUMBER_OF_NEW_TERMS_FIELDS } from '../../../../../constants';
import { MAX_NUMBER_OF_NEW_TERMS_FIELDS } from '../../../../constants';
// Attributes specific to New Terms rules

View file

@ -11,24 +11,13 @@ import {
PositiveIntegerGreaterThanZero,
enumeration,
} from '@kbn/securitysolution-io-ts-types';
/**
* describes how alerts will be generated for documents with missing suppress by fields
*/
export enum AlertSuppressionMissingFieldsStrategy {
// per each document a separate alert will be created
DoNotSuppress = 'doNotSuppress',
// only alert will be created per suppress by bucket
Suppress = 'suppress',
}
import { AlertSuppressionMissingFieldsStrategyEnum } from '../rule_schema/specific_attributes/query_attributes.gen';
export type AlertSuppressionMissingFields = t.TypeOf<typeof AlertSuppressionMissingFields>;
export const AlertSuppressionMissingFields = enumeration(
'AlertSuppressionMissingFields',
AlertSuppressionMissingFieldsStrategy
AlertSuppressionMissingFieldsStrategyEnum
);
export const DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY =
AlertSuppressionMissingFieldsStrategy.Suppress;
export const AlertSuppressionGroupBy = LimitedSizeArray({
codec: t.string,
@ -79,5 +68,3 @@ export const AlertSuppressionCamel = t.intersection([
})
),
]);
export const minimumLicenseForSuppression = 'platinum';

View file

@ -28,15 +28,17 @@ import {
} from '@kbn/securitysolution-io-ts-alerting-types';
import { RuleExecutionSummary } from '../../rule_monitoring/model';
import { ResponseActionArray } from '../rule_response_actions';
// eslint-disable-next-line no-restricted-imports
import { ResponseActionArray } from '../rule_response_actions/response_actions_legacy';
import {
saved_id,
anomaly_threshold,
updated_at,
updated_by,
created_at,
created_by,
revision,
saved_id,
updated_at,
updated_by,
} from '../schemas';
import {
@ -46,6 +48,7 @@ import {
DataViewId,
ExceptionListArray,
IndexPatternArray,
InvestigationFields,
InvestigationGuide,
IsRuleEnabled,
IsRuleImmutable,
@ -53,7 +56,6 @@ import {
RelatedIntegrationArray,
RequiredFieldArray,
RuleAuthorArray,
InvestigationFields,
RuleDescription,
RuleFalsePositiveArray,
RuleFilterArray,
@ -77,16 +79,53 @@ import {
TimestampOverride,
TimestampOverrideFallbackDisabled,
} from './common_attributes';
import {
EventCategoryOverride,
TiebreakerField,
TimestampField,
} from './specific_attributes/eql_attributes';
import { Threshold } from './specific_attributes/threshold_attributes';
import { HistoryWindowStart, NewTermsFields } from './specific_attributes/new_terms_attributes';
import { AlertSuppression } from './specific_attributes/query_attributes';
import { EventCategoryOverride, TiebreakerField, TimestampField } from './eql_attributes';
import { HistoryWindowStart, NewTermsFields } from './new_terms_attributes';
import { AlertSuppression } from './query_attributes';
import { Threshold } from './threshold_attributes';
import { buildRuleSchemas } from './build_rule_schemas';
export const buildRuleSchemas = <
Required extends t.Props,
Optional extends t.Props,
Defaultable extends t.Props
>({
required,
optional,
defaultable,
}: {
required: Required;
optional: Optional;
defaultable: Defaultable;
}) => ({
create: t.intersection([
t.exact(t.type(required)),
t.exact(t.partial(optional)),
t.exact(t.partial(defaultable)),
]),
patch: t.intersection([t.partial(required), t.partial(optional), t.partial(defaultable)]),
response: t.intersection([
t.exact(t.type(required)),
// This bit of logic is to force all fields to be accounted for in conversions from the internal
// rule schema to the response schema. Rather than use `t.partial`, which makes each field optional,
// we make each field required but possibly undefined. The result is that if a field is forgotten in
// the conversion from internal schema to response schema TS will report an error. If we just used t.partial
// instead, then optional fields can be accidentally omitted from the conversion - and any actual values
// in those fields internally will be stripped in the response.
t.exact(t.type(orUndefined(optional))),
t.exact(t.type(defaultable)),
]),
});
export type OrUndefined<P extends t.Props> = {
[K in keyof P]: P[K] | t.UndefinedC;
};
export const orUndefined = <P extends t.Props>(props: P): OrUndefined<P> => {
return Object.keys(props).reduce<t.Props>((acc, key) => {
acc[key] = t.union([props[key], t.undefined]);
return acc;
}, {}) as OrUndefined<P>;
};
// -------------------------------------------------------------------------------------------------
// Base schema
@ -106,7 +145,7 @@ export const baseSchema = buildRuleSchemas({
// Timeline template
timeline_id: TimelineTemplateId,
timeline_title: TimelineTemplateTitle,
// Atributes related to SavedObjectsClient.resolve API
// Attributes related to SavedObjectsClient.resolve API
outcome: SavedObjectResolveOutcome,
alias_target_id: SavedObjectResolveAliasTargetId,
alias_purpose: SavedObjectResolveAliasPurpose,
@ -578,4 +617,4 @@ export type RulePatchProps = t.TypeOf<typeof RulePatchProps>;
export const RulePatchProps = t.intersection([TypeSpecificPatchProps, SharedPatchProps]);
export type RuleResponse = t.TypeOf<typeof RuleResponse>;
export const RuleResponse = t.intersection([SharedResponseProps, TypeSpecificResponse]);
export const RuleResponse = t.intersection([TypeSpecificResponse, SharedResponseProps]);

View file

@ -89,10 +89,6 @@ export const indexRecord = t.record(
})
);
export const indexType = t.type({
index: indexRecord,
});
export const privilege = t.type({
username: t.string,
has_all_requested: t.boolean,

View file

@ -14,5 +14,5 @@ import { z } from 'zod';
export type SortOrder = z.infer<typeof SortOrder>;
export const SortOrder = z.enum(['asc', 'desc']);
export const SortOrderEnum = SortOrder.enum;
export type SortOrderEnum = typeof SortOrder.enum;
export const SortOrderEnum = SortOrder.enum;

View file

@ -8,7 +8,9 @@
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
import { DefaultSortOrderAsc, DefaultSortOrderDesc } from './sorting';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
import { DefaultSortOrderAsc, DefaultSortOrderDesc } from './sorting_legacy';
describe('Common sorting schemas', () => {
describe('DefaultSortOrderAsc', () => {

View file

@ -1,24 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
const partial = t.exact(
t.partial({
buttonLabel: t.string,
})
);
const required = t.exact(
t.type({
type: t.string,
message: t.string,
actionPath: t.string,
})
);
export const WarningSchema = t.intersection([partial, required]);
export type WarningSchema = t.TypeOf<typeof WarningSchema>;

View file

@ -5,8 +5,7 @@
* 2.0.
*/
import { stringifyZodError } from '@kbn/securitysolution-es-utils';
import { expectParseError, expectParseSuccess } from '../../../../test/zod_helpers';
import { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import { GetPrebuiltRulesAndTimelinesStatusResponse } from './get_prebuilt_rules_and_timelines_status_route.gen';
describe('Get prebuilt rules and timelines status response schema', () => {

View file

@ -5,8 +5,7 @@
* 2.0.
*/
import { stringifyZodError } from '@kbn/securitysolution-es-utils';
import { expectParseError, expectParseSuccess } from '../../../../test/zod_helpers';
import { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import { InstallPrebuiltRulesAndTimelinesResponse } from './install_prebuilt_rules_and_timelines_route.gen';
describe('Install prebuilt rules and timelines response schema', () => {

View file

@ -6,7 +6,9 @@
*/
import * as t from 'io-ts';
import { orUndefined } from '../../../../model';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
import { orUndefined } from '../../../../model/rule_schema_legacy';
interface RuleFields<TRequired extends t.Props, TOptional extends t.Props> {
required: TRequired;

View file

@ -7,6 +7,8 @@
import * as t from 'io-ts';
import { TimeDuration } from '@kbn/securitysolution-io-ts-types';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
import {
BuildingBlockType,
DataViewId,
@ -19,8 +21,8 @@ import {
TimelineTemplateTitle,
TimestampOverride as TimestampOverrideFieldName,
TimestampOverrideFallbackDisabled,
saved_id,
} from '../../../../model';
} from '../../../../model/rule_schema_legacy';
import { saved_id } from '../../../../model/schemas';
// -------------------------------------------------------------------------------------------------
// Rule data source

View file

@ -22,6 +22,8 @@ import {
threat_mapping,
} from '@kbn/securitysolution-io-ts-alerting-types';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
import {
AlertSuppression,
EventCategoryOverride,
@ -47,8 +49,7 @@ import {
Threshold,
TiebreakerField,
TimestampField,
anomaly_threshold,
} from '../../../../model';
} from '../../../../model/rule_schema_legacy';
import {
BuildingBlockObject,
@ -64,6 +65,7 @@ import {
} from './diffable_field_types';
import { buildSchema } from './build_schema';
import { anomaly_threshold } from '../../../../model/schemas';
export type DiffableCommonFields = t.TypeOf<typeof DiffableCommonFields>;
export const DiffableCommonFields = buildSchema({

View file

@ -6,7 +6,7 @@
*/
import type { RuleTagArray } from '../../model';
import type { RuleResponse } from '../../model/rule_schema/rule_schemas';
import type { RuleResponse } from '../../model/rule_schema';
export interface ReviewRuleInstallationResponseBody {
/** Aggregated info about all rules available for installation */

View file

@ -7,7 +7,7 @@
import type { RuleObjectId, RuleSignatureId, RuleTagArray } from '../../model';
import type { PartialRuleDiff } from '../model';
import type { RuleResponse } from '../../model/rule_schema/rule_schemas';
import type { RuleResponse } from '../../model/rule_schema';
export interface ReviewRuleUpgradeResponseBody {
/** Aggregated info about all rules available for upgrade */

View file

@ -12,8 +12,9 @@ import type {
ExceptionListItemSchema,
} from '@kbn/securitysolution-io-ts-list-types';
import { createRuleExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { RuleObjectId } from '../../model';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
import { RuleObjectId } from '../../model/rule_schema_legacy';
/**
* URL path parameters of the API route.

View file

@ -13,7 +13,9 @@ import {
DefaultNamespaceArray,
} from '@kbn/securitysolution-io-ts-list-types';
import { NonEmptyStringArray } from '@kbn/securitysolution-io-ts-types';
import { RuleName, RuleObjectId, RuleSignatureId } from '../../model';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
import { RuleName, RuleObjectId, RuleSignatureId } from '../../model/rule_schema_legacy';
// If ids and list_ids are undefined, route will fetch all lists matching the
// specified namespace type

View file

@ -20,13 +20,15 @@ import type { BulkActionSkipResult } from '@kbn/alerting-plugin/common';
import type { RuleResponse } from '../../model';
import type { BulkActionsDryRunErrCode } from '../../../../constants';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
import {
IndexPatternArray,
RuleQuery,
RuleTagArray,
TimelineTemplateId,
TimelineTemplateTitle,
} from '../../model';
} from '../../model/rule_schema_legacy';
export enum BulkActionType {
'enable' = 'enable',

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from 'zod';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
import { RuleCreateProps } from '../../../model/rule_schema/rule_schemas.gen';
import { BulkCrudRulesResponse } from '../response_schema.gen';
export type BulkCreateRulesRequestBody = z.infer<typeof BulkCreateRulesRequestBody>;
export const BulkCreateRulesRequestBody = z.array(RuleCreateProps);
export type BulkCreateRulesRequestBodyInput = z.input<typeof BulkCreateRulesRequestBody>;
export type BulkCreateRulesResponse = z.infer<typeof BulkCreateRulesResponse>;
export const BulkCreateRulesResponse = BulkCrudRulesResponse;

View file

@ -5,8 +5,8 @@ info:
paths:
/api/detection_engine/rules/_bulk_create:
post:
operationId: CreateRulesBulk
x-codegen-enabled: false
operationId: BulkCreateRules
x-codegen-enabled: true
deprecated: true
description: Creates new detection rules in bulk.
tags:

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import { BulkCreateRulesRequestBody } from './bulk_create_rules_route';
import { exactCheck, foldLeftRight, formatErrors } from '@kbn/securitysolution-io-ts-utils';
import { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import { getCreateRulesSchemaMock } from '../../../model/rule_schema/mocks';
import { BulkCreateRulesRequestBody } from './bulk_create_rules_route.gen';
// only the basics of testing are here.
// see: rule_schemas.test.ts for the bulk of the validation tests
@ -16,40 +16,25 @@ describe('Bulk create rules request schema', () => {
test('can take an empty array and validate it', () => {
const payload: BulkCreateRulesRequestBody = [];
const decoded = BulkCreateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(output.errors).toEqual([]);
expect(output.schema).toEqual([]);
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('made up values do not validate for a single element', () => {
const payload: Array<{ madeUp: string }> = [{ madeUp: 'hi' }];
const decoded = BulkCreateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toContain(
'Invalid value "undefined" supplied to "description"'
);
expect(formatErrors(output.errors)).toContain(
'Invalid value "undefined" supplied to "risk_score"'
);
expect(formatErrors(output.errors)).toContain('Invalid value "undefined" supplied to "name"');
expect(formatErrors(output.errors)).toContain(
'Invalid value "undefined" supplied to "severity"'
);
expect(output.schema).toEqual({});
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
});
test('single array element does validate', () => {
const payload: BulkCreateRulesRequestBody = [getCreateRulesSchemaMock()];
const decoded = BulkCreateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('two array elements do validate', () => {
@ -58,11 +43,9 @@ describe('Bulk create rules request schema', () => {
getCreateRulesSchemaMock(),
];
const decoded = BulkCreateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('single array element with a missing value (risk_score) will not validate', () => {
@ -71,13 +54,9 @@ describe('Bulk create rules request schema', () => {
delete singleItem.risk_score;
const payload: BulkCreateRulesRequestBody = [singleItem];
const decoded = BulkCreateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
});
test('two array elements where the first is valid but the second is invalid (risk_score) will not validate', () => {
@ -87,13 +66,9 @@ describe('Bulk create rules request schema', () => {
delete secondItem.risk_score;
const payload: BulkCreateRulesRequestBody = [singleItem, secondItem];
const decoded = BulkCreateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"1: Invalid input"`);
});
test('two array elements where the first is invalid (risk_score) but the second is valid will not validate', () => {
@ -103,13 +78,9 @@ describe('Bulk create rules request schema', () => {
delete singleItem.risk_score;
const payload: BulkCreateRulesRequestBody = [singleItem, secondItem];
const decoded = BulkCreateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
});
test('two array elements where both are invalid (risk_score) will not validate', () => {
@ -121,46 +92,14 @@ describe('Bulk create rules request schema', () => {
delete secondItem.risk_score;
const payload: BulkCreateRulesRequestBody = [singleItem, secondItem];
const decoded = BulkCreateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0: Invalid input, 1: Invalid input"`
);
});
test('two array elements where the first is invalid (extra key and value) but the second is valid will not validate', () => {
const singleItem = {
...getCreateRulesSchemaMock(),
madeUpValue: 'something',
};
const secondItem = getCreateRulesSchemaMock();
const payload: BulkCreateRulesRequestBody = [singleItem, secondItem];
const decoded = BulkCreateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue"']);
expect(output.schema).toEqual({});
});
test('two array elements where the second is invalid (extra key and value) but the first is valid will not validate', () => {
const singleItem = getCreateRulesSchemaMock();
const secondItem = {
...getCreateRulesSchemaMock(),
madeUpValue: 'something',
};
const payload: BulkCreateRulesRequestBody = [singleItem, secondItem];
const decoded = BulkCreateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue"']);
expect(output.schema).toEqual({});
});
test('two array elements where both are invalid (extra key and value) will not validate', () => {
test('extra keys are omitted from the payload', () => {
const singleItem = {
...getCreateRulesSchemaMock(),
madeUpValue: 'something',
@ -171,22 +110,18 @@ describe('Bulk create rules request schema', () => {
};
const payload: BulkCreateRulesRequestBody = [singleItem, secondItem];
const decoded = BulkCreateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue,madeUpValue"']);
expect(output.schema).toEqual({});
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual([getCreateRulesSchemaMock(), getCreateRulesSchemaMock()]);
});
test('You cannot set the severity to a value other than low, medium, high, or critical', () => {
const badSeverity = { ...getCreateRulesSchemaMock(), severity: 'madeup' };
const payload = [badSeverity];
const decoded = BulkCreateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['Invalid value "madeup" supplied to "severity"']);
expect(output.schema).toEqual({});
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
});
test('You can set "note" to a string', () => {
@ -194,21 +129,17 @@ describe('Bulk create rules request schema', () => {
{ ...getCreateRulesSchemaMock(), note: '# test markdown' },
];
const decoded = BulkCreateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('You can set "note" to an empty string', () => {
const payload: BulkCreateRulesRequestBody = [{ ...getCreateRulesSchemaMock(), note: '' }];
const decoded = BulkCreateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('You cant set "note" to anything other than string', () => {
@ -221,12 +152,8 @@ describe('Bulk create rules request schema', () => {
},
];
const decoded = BulkCreateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "{"something":"some object"}" supplied to "note"',
]);
expect(output.schema).toEqual({});
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
});
});

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { RuleCreateProps } from '../../../model';
/**
* Request body parameters of the API route.
*/
export type BulkCreateRulesRequestBody = t.TypeOf<typeof BulkCreateRulesRequestBody>;
export const BulkCreateRulesRequestBody = t.array(RuleCreateProps);

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from 'zod';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
import { RuleObjectId, RuleSignatureId } from '../../../model/rule_schema/common_attributes.gen';
import { BulkCrudRulesResponse } from '../response_schema.gen';
export type BulkDeleteRulesRequestBody = z.infer<typeof BulkDeleteRulesRequestBody>;
export const BulkDeleteRulesRequestBody = z.array(
z.object({
id: RuleObjectId.optional(),
rule_id: RuleSignatureId.optional(),
})
);
export type BulkDeleteRulesRequestBodyInput = z.input<typeof BulkDeleteRulesRequestBody>;
export type BulkDeleteRulesResponse = z.infer<typeof BulkDeleteRulesResponse>;
export const BulkDeleteRulesResponse = BulkCrudRulesResponse;

View file

@ -5,8 +5,8 @@ info:
paths:
/api/detection_engine/rules/_bulk_delete:
delete:
operationId: DeleteRulesBulk
x-codegen-enabled: false
operationId: BulkDeleteRules
x-codegen-enabled: true
deprecated: true
description: Deletes multiple rules.
tags:

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { exactCheck, formatErrors, foldLeftRight } from '@kbn/securitysolution-io-ts-utils';
import { BulkDeleteRulesRequestBody } from './bulk_delete_rules_route';
import { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import { BulkDeleteRulesRequestBody } from './bulk_delete_rules_route.gen';
// only the basics of testing are here.
// see: query_rules_schema.test.ts for the bulk of the validation tests
@ -15,11 +15,9 @@ describe('Bulk delete rules request schema', () => {
test('can take an empty array and validate it', () => {
const payload: BulkDeleteRulesRequestBody = [];
const decoded = BulkDeleteRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual([]);
const result = BulkDeleteRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('non uuid being supplied to id does not validate', () => {
@ -29,11 +27,9 @@ describe('Bulk delete rules request schema', () => {
},
];
const decoded = BulkDeleteRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['Invalid value "1" supplied to "id"']);
expect(output.schema).toEqual({});
const result = BulkDeleteRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0.id: Invalid uuid"`);
});
test('both rule_id and id being supplied do validate', () => {
@ -44,11 +40,9 @@ describe('Bulk delete rules request schema', () => {
},
];
const decoded = BulkDeleteRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkDeleteRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('only id validates with two elements', () => {
@ -57,11 +51,9 @@ describe('Bulk delete rules request schema', () => {
{ id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' },
];
const decoded = BulkDeleteRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkDeleteRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('only rule_id validates', () => {
@ -69,11 +61,9 @@ describe('Bulk delete rules request schema', () => {
{ rule_id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' },
];
const decoded = BulkDeleteRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkDeleteRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('only rule_id validates with two elements', () => {
@ -82,11 +72,9 @@ describe('Bulk delete rules request schema', () => {
{ rule_id: '2' },
];
const decoded = BulkDeleteRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkDeleteRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('both id and rule_id validates with two separate elements', () => {
@ -95,10 +83,8 @@ describe('Bulk delete rules request schema', () => {
{ rule_id: '2' },
];
const decoded = BulkDeleteRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkDeleteRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
});

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { QueryRuleByIds } from '../../model/query_rule_by_ids';
/**
* Request body parameters of the API route.
*/
export type BulkDeleteRulesRequestBody = t.TypeOf<typeof BulkDeleteRulesRequestBody>;
export const BulkDeleteRulesRequestBody = t.array(QueryRuleByIds);

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from 'zod';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
import { RulePatchProps } from '../../../model/rule_schema/rule_schemas.gen';
import { BulkCrudRulesResponse } from '../response_schema.gen';
export type BulkPatchRulesRequestBody = z.infer<typeof BulkPatchRulesRequestBody>;
export const BulkPatchRulesRequestBody = z.array(RulePatchProps);
export type BulkPatchRulesRequestBodyInput = z.input<typeof BulkPatchRulesRequestBody>;
export type BulkPatchRulesResponse = z.infer<typeof BulkPatchRulesResponse>;
export const BulkPatchRulesResponse = BulkCrudRulesResponse;

View file

@ -5,8 +5,8 @@ info:
paths:
/api/detection_engine/rules/_bulk_update:
patch:
operationId: PatchRulesBulk
x-codegen-enabled: false
operationId: BulkPatchRules
x-codegen-enabled: true
deprecated: true
description: Updates multiple rules using the `PATCH` method.
tags:

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import { exactCheck, formatErrors, foldLeftRight } from '@kbn/securitysolution-io-ts-utils';
import type { PatchRuleRequestBody } from '../../crud/patch_rule/patch_rule_route';
import { BulkPatchRulesRequestBody } from './bulk_patch_rules_route';
import { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import type { PatchRuleRequestBody } from '../../crud/patch_rule/patch_rule_route.gen';
import { BulkPatchRulesRequestBody } from './bulk_patch_rules_route.gen';
// only the basics of testing are here.
// see: patch_rules_schema.test.ts for the bulk of the validation tests
@ -16,21 +16,17 @@ describe('Bulk patch rules request schema', () => {
test('can take an empty array and validate it', () => {
const payload: BulkPatchRulesRequestBody = [];
const decoded = BulkPatchRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(output.errors).toEqual([]);
expect(output.schema).toEqual([]);
const result = BulkPatchRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('single array of [id] does validate', () => {
const payload: BulkPatchRulesRequestBody = [{ id: '4125761e-51da-4de9-a0c8-42824f532ddb' }];
const decoded = BulkPatchRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkPatchRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('two arrays of [id] validate', () => {
@ -39,11 +35,9 @@ describe('Bulk patch rules request schema', () => {
{ id: '192f403d-b285-4251-9e8b-785fcfcf22e8' },
];
const decoded = BulkPatchRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkPatchRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('can set "note" to be a string', () => {
@ -52,11 +46,9 @@ describe('Bulk patch rules request schema', () => {
{ note: 'hi' },
];
const decoded = BulkPatchRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkPatchRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('can set "note" to be an empty string', () => {
@ -65,11 +57,9 @@ describe('Bulk patch rules request schema', () => {
{ note: '' },
];
const decoded = BulkPatchRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkPatchRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('cannot set "note" to be anything other than a string', () => {
@ -78,12 +68,8 @@ describe('Bulk patch rules request schema', () => {
{ note: { someprop: 'some value here' } },
];
const decoded = BulkPatchRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "{"someprop":"some value here"}" supplied to "note"',
]);
expect(output.schema).toEqual({});
const result = BulkPatchRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"1: Invalid input"`);
});
});

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { RulePatchProps } from '../../../model';
/**
* Request body parameters of the API route.
*/
export type BulkPatchRulesRequestBody = t.TypeOf<typeof BulkPatchRulesRequestBody>;
export const BulkPatchRulesRequestBody = t.array(RulePatchProps);

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from 'zod';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
import { RuleUpdateProps } from '../../../model/rule_schema/rule_schemas.gen';
import { BulkCrudRulesResponse } from '../response_schema.gen';
export type BulkUpdateRulesRequestBody = z.infer<typeof BulkUpdateRulesRequestBody>;
export const BulkUpdateRulesRequestBody = z.array(RuleUpdateProps);
export type BulkUpdateRulesRequestBodyInput = z.input<typeof BulkUpdateRulesRequestBody>;
export type BulkUpdateRulesResponse = z.infer<typeof BulkUpdateRulesResponse>;
export const BulkUpdateRulesResponse = BulkCrudRulesResponse;

View file

@ -5,8 +5,8 @@ info:
paths:
/api/detection_engine/rules/_bulk_update:
put:
operationId: UpdateRulesBulk
x-codegen-enabled: false
operationId: BulkUpdateRules
x-codegen-enabled: true
deprecated: true
description: Updates multiple rules using the `PUT` method.
tags:

View file

@ -5,10 +5,10 @@
* 2.0.
*/
import { exactCheck, formatErrors, foldLeftRight } from '@kbn/securitysolution-io-ts-utils';
import { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import type { RuleUpdateProps } from '../../../model';
import { getUpdateRulesSchemaMock } from '../../../model/rule_schema/mocks';
import { BulkUpdateRulesRequestBody } from './bulk_update_rules_route';
import { BulkUpdateRulesRequestBody } from './bulk_update_rules_route.gen';
// only the basics of testing are here.
// see: update_rules_schema.test.ts for the bulk of the validation tests
@ -17,40 +17,25 @@ describe('Bulk update rules request schema', () => {
test('can take an empty array and validate it', () => {
const payload: BulkUpdateRulesRequestBody = [];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(output.errors).toEqual([]);
expect(output.schema).toEqual([]);
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('made up values do not validate for a single element', () => {
const payload: Array<{ madeUp: string }> = [{ madeUp: 'hi' }];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toContain(
'Invalid value "undefined" supplied to "description"'
);
expect(formatErrors(output.errors)).toContain(
'Invalid value "undefined" supplied to "risk_score"'
);
expect(formatErrors(output.errors)).toContain('Invalid value "undefined" supplied to "name"');
expect(formatErrors(output.errors)).toContain(
'Invalid value "undefined" supplied to "severity"'
);
expect(output.schema).toEqual({});
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
});
test('single array element does validate', () => {
const payload: BulkUpdateRulesRequestBody = [getUpdateRulesSchemaMock()];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('two array elements do validate', () => {
@ -59,11 +44,9 @@ describe('Bulk update rules request schema', () => {
getUpdateRulesSchemaMock(),
];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('single array element with a missing value (risk_score) will not validate', () => {
@ -72,13 +55,9 @@ describe('Bulk update rules request schema', () => {
delete singleItem.risk_score;
const payload: BulkUpdateRulesRequestBody = [singleItem];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
});
test('two array elements where the first is valid but the second is invalid (risk_score) will not validate', () => {
@ -88,13 +67,9 @@ describe('Bulk update rules request schema', () => {
delete secondItem.risk_score;
const payload: BulkUpdateRulesRequestBody = [singleItem, secondItem];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"1: Invalid input"`);
});
test('two array elements where the first is invalid (risk_score) but the second is valid will not validate', () => {
@ -104,13 +79,9 @@ describe('Bulk update rules request schema', () => {
delete singleItem.risk_score;
const payload: BulkUpdateRulesRequestBody = [singleItem, secondItem];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
});
test('two array elements where both are invalid (risk_score) will not validate', () => {
@ -122,46 +93,14 @@ describe('Bulk update rules request schema', () => {
delete secondItem.risk_score;
const payload: BulkUpdateRulesRequestBody = [singleItem, secondItem];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "undefined" supplied to "risk_score"',
]);
expect(output.schema).toEqual({});
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0: Invalid input, 1: Invalid input"`
);
});
test('two array elements where the first is invalid (extra key and value) but the second is valid will not validate', () => {
const singleItem: RuleUpdateProps & { madeUpValue: string } = {
...getUpdateRulesSchemaMock(),
madeUpValue: 'something',
};
const secondItem = getUpdateRulesSchemaMock();
const payload = [singleItem, secondItem];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue"']);
expect(output.schema).toEqual({});
});
test('two array elements where the second is invalid (extra key and value) but the first is valid will not validate', () => {
const singleItem: RuleUpdateProps = getUpdateRulesSchemaMock();
const secondItem: RuleUpdateProps & { madeUpValue: string } = {
...getUpdateRulesSchemaMock(),
madeUpValue: 'something',
};
const payload: BulkUpdateRulesRequestBody = [singleItem, secondItem];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue"']);
expect(output.schema).toEqual({});
});
test('two array elements where both are invalid (extra key and value) will not validate', () => {
test('extra props will be omitted from the payload after validation', () => {
const singleItem: RuleUpdateProps & { madeUpValue: string } = {
...getUpdateRulesSchemaMock(),
madeUpValue: 'something',
@ -172,22 +111,18 @@ describe('Bulk update rules request schema', () => {
};
const payload: BulkUpdateRulesRequestBody = [singleItem, secondItem];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['invalid keys "madeUpValue,madeUpValue"']);
expect(output.schema).toEqual({});
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual([getUpdateRulesSchemaMock(), getUpdateRulesSchemaMock()]);
});
test('You cannot set the severity to a value other than low, medium, high, or critical', () => {
const badSeverity = { ...getUpdateRulesSchemaMock(), severity: 'madeup' };
const payload = [badSeverity];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual(['Invalid value "madeup" supplied to "severity"']);
expect(output.schema).toEqual({});
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
});
test('You can set "namespace" to a string', () => {
@ -195,11 +130,9 @@ describe('Bulk update rules request schema', () => {
{ ...getUpdateRulesSchemaMock(), namespace: 'a namespace' },
];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('You can set "note" to a string', () => {
@ -207,21 +140,17 @@ describe('Bulk update rules request schema', () => {
{ ...getUpdateRulesSchemaMock(), note: '# test markdown' },
];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('You can set "note" to an empty string', () => {
const payload: BulkUpdateRulesRequestBody = [{ ...getUpdateRulesSchemaMock(), note: '' }];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([]);
expect(output.schema).toEqual(payload);
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('You cant set "note" to anything other than string', () => {
@ -234,12 +163,8 @@ describe('Bulk update rules request schema', () => {
},
];
const decoded = BulkUpdateRulesRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
const output = foldLeftRight(checked);
expect(formatErrors(output.errors)).toEqual([
'Invalid value "{"something":"some object"}" supplied to "note"',
]);
expect(output.schema).toEqual({});
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
});
});

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { RuleUpdateProps } from '../../../model';
/**
* Request body parameters of the API route.
*/
export type BulkUpdateRulesRequestBody = t.TypeOf<typeof BulkUpdateRulesRequestBody>;
export const BulkUpdateRulesRequestBody = t.array(RuleUpdateProps);

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from 'zod';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
import { RuleResponse } from '../../model/rule_schema/rule_schemas.gen';
import { ErrorSchema } from '../../model/error_schema.gen';
export type BulkCrudRulesResponse = z.infer<typeof BulkCrudRulesResponse>;
export const BulkCrudRulesResponse = z.array(z.union([RuleResponse, ErrorSchema]));

View file

@ -4,7 +4,7 @@ info:
version: 8.9.0
paths: {}
components:
x-codegen-enabled: false
x-codegen-enabled: true
schemas:
BulkCrudRulesResponse:
type: array

View file

@ -5,45 +5,36 @@
* 2.0.
*/
import { left } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
import type { RuleResponse, ErrorSchema } from '../../model';
import { getRulesSchemaMock } from '../../model/rule_schema/mocks';
import type { ErrorSchema, RuleResponse } from '../../model';
import { getErrorSchemaMock } from '../../model/error_schema.mock';
import { getRulesSchemaMock } from '../../model/rule_schema/mocks';
import { BulkCrudRulesResponse } from './response_schema';
import { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import { BulkCrudRulesResponse } from './response_schema.gen';
describe('Bulk CRUD rules response schema', () => {
test('it should validate a regular message and and error together with a uuid', () => {
const payload: BulkCrudRulesResponse = [getRulesSchemaMock(), getErrorSchemaMock()];
const decoded = BulkCrudRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([getRulesSchemaMock(), getErrorSchemaMock()]);
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should validate a regular message and and error together when the error has a non UUID', () => {
test('it should validate a regular message and error together when the error has a non UUID', () => {
const payload: BulkCrudRulesResponse = [getRulesSchemaMock(), getErrorSchemaMock('fake id')];
const decoded = BulkCrudRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([getRulesSchemaMock(), getErrorSchemaMock('fake id')]);
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should validate an error', () => {
const payload: BulkCrudRulesResponse = [getErrorSchemaMock('fake id')];
const decoded = BulkCrudRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual([getErrorSchemaMock('fake id')]);
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should NOT validate a rule with a deleted value', () => {
@ -51,15 +42,10 @@ describe('Bulk CRUD rules response schema', () => {
// @ts-expect-error
delete rule.name;
const payload: BulkCrudRulesResponse = [rule];
const decoded = BulkCrudRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "name"',
'Invalid value "undefined" supplied to "error"',
]);
expect(message.schema).toEqual({});
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
});
test('it should NOT validate an invalid error message with a deleted value', () => {
@ -67,38 +53,30 @@ describe('Bulk CRUD rules response schema', () => {
// @ts-expect-error
delete error.error;
const payload: BulkCrudRulesResponse = [error];
const decoded = BulkCrudRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toContain(
'Invalid value "undefined" supplied to "error"'
);
expect(message.schema).toEqual({});
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
});
test('it should NOT validate a type of "query" when it has extra data', () => {
test('it should omit any extra rule props', () => {
const rule: RuleResponse & { invalid_extra_data?: string } = getRulesSchemaMock();
rule.invalid_extra_data = 'invalid_extra_data';
const payload: BulkCrudRulesResponse = [rule];
const decoded = BulkCrudRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']);
expect(message.schema).toEqual({});
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual([getRulesSchemaMock()]);
});
test('it should NOT validate a type of "query" when it has extra data next to a valid error', () => {
const rule: RuleResponse & { invalid_extra_data?: string } = getRulesSchemaMock();
rule.invalid_extra_data = 'invalid_extra_data';
const payload: BulkCrudRulesResponse = [getErrorSchemaMock(), rule];
const decoded = BulkCrudRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']);
expect(message.schema).toEqual({});
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual([getErrorSchemaMock(), getRulesSchemaMock()]);
});
test('it should NOT validate an error when it has extra data', () => {
@ -106,12 +84,12 @@ describe('Bulk CRUD rules response schema', () => {
const error: InvalidError = getErrorSchemaMock();
error.invalid_extra_data = 'invalid';
const payload: BulkCrudRulesResponse = [error];
const decoded = BulkCrudRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']);
expect(message.schema).toEqual({});
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0: Unrecognized key(s) in object: 'invalid_extra_data'"`
);
});
test('it should NOT validate an error when it has extra data next to a valid payload element', () => {
@ -119,11 +97,11 @@ describe('Bulk CRUD rules response schema', () => {
const error: InvalidError = getErrorSchemaMock();
error.invalid_extra_data = 'invalid';
const payload: BulkCrudRulesResponse = [getRulesSchemaMock(), error];
const decoded = BulkCrudRulesResponse.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_extra_data"']);
expect(message.schema).toEqual({});
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"1: Unrecognized key(s) in object: 'invalid_extra_data'"`
);
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { z } from 'zod';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
import { RuleCreateProps, RuleResponse } from '../../../model/rule_schema/rule_schemas.gen';
export type CreateRuleRequestBody = z.infer<typeof CreateRuleRequestBody>;
export const CreateRuleRequestBody = RuleCreateProps;
export type CreateRuleRequestBodyInput = z.input<typeof CreateRuleRequestBody>;
export type CreateRuleResponse = z.infer<typeof CreateRuleResponse>;
export const CreateRuleResponse = RuleResponse;

View file

@ -6,7 +6,7 @@ paths:
/api/detection_engine/rules:
post:
operationId: CreateRule
x-codegen-enabled: false
x-codegen-enabled: true
description: Create a single detection rule
tags:
- Rules API

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type * as t from 'io-ts';
import { RuleCreateProps, RuleResponse } from '../../../model';
export const CreateRuleRequestBody = RuleCreateProps;
export type CreateRuleRequestBody = t.TypeOf<typeof CreateRuleRequestBody>;
export const CreateRuleResponse = RuleResponse;
export type CreateRuleResponse = t.TypeOf<typeof CreateRuleResponse>;

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from 'zod';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
import { RuleObjectId, RuleSignatureId } from '../../../model/rule_schema/common_attributes.gen';
import { RuleResponse } from '../../../model/rule_schema/rule_schemas.gen';
export type DeleteRuleRequestQuery = z.infer<typeof DeleteRuleRequestQuery>;
export const DeleteRuleRequestQuery = z.object({
/**
* The rule's `id` value.
*/
id: RuleObjectId.optional(),
/**
* The rule's `rule_id` value.
*/
rule_id: RuleSignatureId.optional(),
});
export type DeleteRuleRequestQueryInput = z.input<typeof DeleteRuleRequestQuery>;
export type DeleteRuleResponse = z.infer<typeof DeleteRuleResponse>;
export const DeleteRuleResponse = RuleResponse;

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