[Security Solution] Migrate remaining public Detection Engine APIs to OpenAPI and code generation (#170330)

**Related to: https://github.com/elastic/security-team/issues/7491**

## Summary

Migrated remaining public Detection Engine endpoints to OpenAPI schema
and code generation:

- `POST /api/detection_engine/rules/_bulk_action`
- `GET /api/detection_engine/rules/_find`

 Also completed the migration of internal APIs:

- `GET /internal/detection_engine/rules/{ruleId}/execution/events`
- `GET /internal/detection_engine/rules/{ruleId}/execution/results`

### Other notable changes

- Changed how we compose Zod error messages for unions, see
`packages/kbn-zod-helpers/src/stringify_zod_error.ts`. Now we are trying
to list the validation errors of all union members but limiting the
total number of validation errors displayed to users.
- Addressed some remaining `TODO
https://github.com/elastic/security-team/issues/7491`
- Removed dependencies of the risk engine and timelines on detection
engine schemas
- Removed outdated legacy rule schemas that are no longer in use
- Added new schema helpers that work with query params:
`BooleanFromString` and `ArrayFromString`

![image](f4898f11-04e2-4c82-bce9-e662ba78f724)

![image](235234e7-c86c-49a1-b39f-6f9f8dc780e7)
This commit is contained in:
Dmitrii Shevchenko 2023-11-08 11:58:28 +01:00 committed by GitHub
parent 0063691ad5
commit e00566fa98
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
164 changed files with 2567 additions and 2583 deletions

View file

@ -6,7 +6,7 @@
*/
import { z } from "zod";
import { requiredOptional, isValidDateMath } from "@kbn/zod-helpers"
import { requiredOptional, isValidDateMath, ArrayFromString, BooleanFromString } from "@kbn/zod-helpers"
{{> disclaimer}}

View file

@ -19,10 +19,7 @@
{{~/if~}}
{{~#if (eq type "array")}}
z.preprocess(
(value: unknown) => (typeof value === "string") ? value === '' ? [] : value.split(",") : value,
z.array({{~> zod_schema_item items ~}})
)
ArrayFromString({{~> zod_schema_item items ~}})
{{~#if minItems}}.min({{minItems}}){{/if~}}
{{~#if maxItems}}.max({{maxItems}}){{/if~}}
{{~#if (eq requiredBool false)}}.optional(){{/if~}}
@ -30,12 +27,9 @@
{{~/if~}}
{{~#if (eq type "boolean")}}
z.preprocess(
(value: unknown) => (typeof value === "boolean") ? String(value) : value,
z.enum(["true", "false"])
{{~#if (defined default)}}.default("{{{toJSON default}}}"){{/if~}}
.transform((value) => value === "true")
)
BooleanFromString
{{~#if (eq requiredBool false)}}.optional(){{/if~}}
{{~#if (defined default)}}.default({{{toJSON default}}}){{/if~}}
{{~/if~}}
{{~#if (eq type "string")}}

View file

@ -6,8 +6,11 @@
* Side Public License, v 1.
*/
export * from './src/array_from_string';
export * from './src/boolean_from_string';
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/safe_parse_result';
export * from './src/stringify_zod_error';

View file

@ -0,0 +1,34 @@
/*
* 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 { ArrayFromString } from './array_from_string';
import * as z from 'zod';
describe('ArrayFromString', () => {
const itemsSchema = z.string();
it('should return an array when input is a string', () => {
const result = ArrayFromString(itemsSchema).parse('a,b,c');
expect(result).toEqual(['a', 'b', 'c']);
});
it('should return an empty array when input is an empty string', () => {
const result = ArrayFromString(itemsSchema).parse('');
expect(result).toEqual([]);
});
it('should return the input as is when it is not a string', () => {
const input = ['a', 'b', 'c'];
const result = ArrayFromString(itemsSchema).parse(input);
expect(result).toEqual(input);
});
it('should throw an error when input is not a string or an array', () => {
expect(() => ArrayFromString(itemsSchema).parse(123)).toThrow();
});
});

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 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';
/**
* This is a helper schema to convert comma separated strings to arrays. Useful
* for processing query params.
*
* @param schema Array items schema
* @returns Array schema that accepts a comma-separated string as input
*/
export function ArrayFromString<T extends z.ZodTypeAny>(schema: T) {
return z.preprocess(
(value: unknown) =>
typeof value === 'string' ? (value === '' ? [] : value.split(',')) : value,
z.array(schema)
);
}

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 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 { BooleanFromString } from './boolean_from_string';
describe('BooleanFromString', () => {
it('should return true when input is "true"', () => {
expect(BooleanFromString.parse('true')).toBe(true);
});
it('should return false when input is "false"', () => {
expect(BooleanFromString.parse('false')).toBe(false);
});
it('should return true when input is true', () => {
expect(BooleanFromString.parse(true)).toBe(true);
});
it('should return false when input is false', () => {
expect(BooleanFromString.parse(false)).toBe(false);
});
it('should throw an error when input is not a boolean or "true" or "false"', () => {
expect(() => BooleanFromString.parse('not a boolean')).toThrow();
expect(() => BooleanFromString.parse(42)).toThrow();
});
});

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 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';
/**
* This is a helper schema to convert a boolean string ("true" or "false") to a
* boolean. Useful for processing query params.
*
* Accepts "true" or "false" as strings, or a boolean.
*/
export const BooleanFromString = z
.enum(['true', 'false'])
.or(z.boolean())
.transform((value) => {
if (typeof value === 'boolean') {
return value;
}
return value === 'true';
});

View file

@ -7,9 +7,14 @@
*/
import type { SafeParseReturnType, SafeParseSuccess } from 'zod';
import { stringifyZodError } from './stringify_zod_error';
export function expectParseSuccess<Input, Output>(
result: SafeParseReturnType<Input, Output>
): asserts result is SafeParseSuccess<Output> {
expect(result.success).toEqual(true);
if (!result.success) {
// We are throwing here instead of using assertions because we want to show
// the stringified error to assist with debugging.
throw new Error(`Expected parse success, got error: ${stringifyZodError(result.error)}`);
}
}

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 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';
/**
* Safely parse a payload against a schema, returning the output or undefined.
* This method does not throw validation errors and is useful for validating
* optional objects when we don't care about errors.
*
* @param payload Schema payload
* @param schema Validation schema
* @returns Schema output or undefined
*/
export function safeParseResult<T extends z.ZodTypeAny>(
payload: unknown,
schema: T
): T['_output'] | undefined {
const result = schema.safeParse(payload);
if (result.success) {
return result.data;
}
}

View file

@ -6,16 +6,41 @@
* Side Public License, v 1.
*/
import { ZodError } from 'zod';
import { ZodError, ZodIssue } from 'zod';
const MAX_ERRORS = 5;
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(', ');
const errorMessages: string[] = [];
const issues = err.issues;
// Recursively traverse all issues
while (issues.length > 0) {
const issue = issues.shift()!;
// If the issue is an invalid union, we need to traverse all issues in the
// "unionErrors" array
if (issue.code === 'invalid_union') {
issues.push(...issue.unionErrors.flatMap((e) => e.issues));
continue;
}
errorMessages.push(stringifyIssue(issue));
}
const extraErrorCount = errorMessages.length - MAX_ERRORS;
if (extraErrorCount > 0) {
errorMessages.splice(MAX_ERRORS);
errorMessages.push(`and ${extraErrorCount} more`);
}
return errorMessages.join(', ');
}
function stringifyIssue(issue: ZodIssue) {
if (issue.path.length === 0) {
return issue.message;
}
return `${issue.path.join('.')}: ${issue.message}`;
}

View file

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

View file

@ -8,12 +8,8 @@
export * from './alerts';
export * from './rule_response_actions';
export * from './rule_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 './error_schema.gen';
export * from './pagination.gen';
export * from './schemas';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
export * from './sorting_legacy';
export * from './sorting.gen';
export * from './warning_schema.gen';

View file

@ -0,0 +1,35 @@
/*
* 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.
*/
/**
* Page number
*/
export type Page = z.infer<typeof Page>;
export const Page = z.number().int().min(1);
/**
* Number of items per page
*/
export type PerPage = z.infer<typeof PerPage>;
export const PerPage = z.number().int().min(0);
export type PaginationResult = z.infer<typeof PaginationResult>;
export const PaginationResult = z.object({
page: Page,
per_page: PerPage,
/**
* Total number of items
*/
total: z.number().int().min(0),
});

View file

@ -0,0 +1,31 @@
openapi: 3.0.0
info:
title: Pagination Schema
version: 'not applicable'
paths: {}
components:
x-codegen-enabled: true
schemas:
Page:
type: integer
minimum: 1
description: Page number
PerPage:
type: integer
minimum: 0
description: Number of items per page
PaginationResult:
type: object
properties:
page:
$ref: '#/components/schemas/Page'
per_page:
$ref: '#/components/schemas/PerPage'
total:
type: integer
minimum: 0
description: Total number of items
required:
- page
- per_page
- total

View file

@ -1,28 +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 { PositiveInteger, PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types';
export type Page = t.TypeOf<typeof Page>;
export const Page = PositiveIntegerGreaterThanZero;
export type PageOrUndefined = t.TypeOf<typeof PageOrUndefined>;
export const PageOrUndefined = t.union([Page, t.undefined]);
export type PerPage = t.TypeOf<typeof PerPage>;
export const PerPage = PositiveInteger;
export type PerPageOrUndefined = t.TypeOf<typeof PerPageOrUndefined>;
export const PerPageOrUndefined = t.union([PerPage, t.undefined]);
export type PaginationResult = t.TypeOf<typeof PaginationResult>;
export const PaginationResult = t.type({
page: Page,
per_page: PerPage,
total: PositiveInteger,
});

View file

@ -5,7 +5,4 @@
* 2.0.
*/
// 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

@ -367,26 +367,38 @@ export const RuleActionFrequency = z.object({
throttle: RuleActionThrottle.nullable(),
});
export type RuleActionAlertsFilter = z.infer<typeof RuleActionAlertsFilter>;
export const RuleActionAlertsFilter = z.object({}).catchall(z.unknown());
/**
* Object containing the allowed connector fields, which varies according to the connector type.
*/
export type RuleActionParams = z.infer<typeof RuleActionParams>;
export const RuleActionParams = z.object({}).catchall(z.unknown());
/**
* Optionally groups actions by use cases. Use `default` for alert notifications.
*/
export type RuleActionGroup = z.infer<typeof RuleActionGroup>;
export const RuleActionGroup = z.string();
/**
* The connector ID.
*/
export type RuleActionId = z.infer<typeof RuleActionId>;
export const RuleActionId = z.string();
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()),
group: RuleActionGroup,
id: RuleActionId,
params: RuleActionParams,
uuid: NonEmptyString.optional(),
alerts_filter: z.object({}).catchall(z.unknown()).optional(),
alerts_filter: RuleActionAlertsFilter.optional(),
frequency: RuleActionFrequency.optional(),
});

View file

@ -397,6 +397,23 @@ components:
- notifyWhen
- throttle
RuleActionAlertsFilter:
type: object
additionalProperties: true
RuleActionParams:
type: object
description: Object containing the allowed connector fields, which varies according to the connector type.
additionalProperties: true
RuleActionGroup:
type: string
description: Optionally groups actions by use cases. Use `default` for alert notifications.
RuleActionId:
type: string
description: The connector ID.
RuleAction:
type: object
properties:
@ -404,20 +421,15 @@ components:
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.
$ref: '#/components/schemas/RuleActionGroup'
id:
type: string
description: The connector ID.
$ref: '#/components/schemas/RuleActionId'
params:
type: object
description: Object containing the allowed connector fields, which varies according to the connector type.
additionalProperties: true
$ref: '#/components/schemas/RuleActionParams'
uuid:
$ref: '#/components/schemas/NonEmptyString'
alerts_filter:
type: object
additionalProperties: true
$ref: '#/components/schemas/RuleActionAlertsFilter'
frequency:
$ref: '#/components/schemas/RuleActionFrequency'
required:

View file

@ -25,7 +25,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"name: Required, description: Required, risk_score: Required, severity: Required, type: Invalid literal value, expected \\"eql\\", and 52 more"`
);
});
test('strips any unknown values', () => {
@ -46,7 +48,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"name: Required, description: Required, risk_score: Required, severity: Required, type: Invalid literal value, expected \\"eql\\", and 52 more"`
);
});
test('[rule_id, description] does not validate', () => {
@ -57,7 +61,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"name: Required, risk_score: Required, severity: Required, type: Invalid literal value, expected \\"eql\\", query: Required, and 44 more"`
);
});
test('[rule_id, description, from] does not validate', () => {
@ -69,7 +75,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"name: Required, risk_score: Required, severity: Required, type: Invalid literal value, expected \\"eql\\", query: Required, and 44 more"`
);
});
test('[rule_id, description, from, to] does not validate', () => {
@ -82,7 +90,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"name: Required, risk_score: Required, severity: Required, type: Invalid literal value, expected \\"eql\\", query: Required, and 44 more"`
);
});
test('[rule_id, description, from, to, name] does not validate', () => {
@ -96,7 +106,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"risk_score: Required, severity: Required, type: Invalid literal value, expected \\"eql\\", query: Required, language: Invalid literal value, expected \\"eql\\", and 36 more"`
);
});
test('[rule_id, description, from, to, name, severity] does not validate', () => {
@ -111,7 +123,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"risk_score: Required, type: Invalid literal value, expected \\"eql\\", query: Required, language: Invalid literal value, expected \\"eql\\", risk_score: Required, and 28 more"`
);
});
test('[rule_id, description, from, to, name, severity, type] does not validate', () => {
@ -127,7 +141,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"risk_score: Required, type: Invalid literal value, expected \\"eql\\", query: Required, language: Invalid literal value, expected \\"eql\\", risk_score: Required, and 27 more"`
);
});
test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => {
@ -144,7 +160,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"risk_score: Required, type: Invalid literal value, expected \\"eql\\", query: Required, language: Invalid literal value, expected \\"eql\\", risk_score: Required, and 27 more"`
);
});
test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => {
@ -162,7 +180,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"risk_score: Required, type: Invalid literal value, expected \\"eql\\", query: Required, language: Invalid literal value, expected \\"eql\\", risk_score: Required, and 27 more"`
);
});
test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => {
@ -202,7 +222,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"risk_score: Required, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", risk_score: Required, risk_score: Required, and 22 more"`
);
});
test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => {
@ -368,7 +390,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"references.0: Expected string, received number, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", references.0: Expected string, received number, references.0: Expected string, received number, and 22 more"`
);
});
test('indexes cannot be numbers', () => {
@ -379,7 +403,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", index.0: Expected string, received number, index.0: Expected string, received number, type: Invalid literal value, expected \\"saved_query\\", and 20 more"`
);
});
test('saved_query type can have filters with it', () => {
@ -401,7 +427,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", filters: Expected array, received string, filters: Expected array, received string, type: Invalid literal value, expected \\"saved_query\\", and 20 more"`
);
});
test('language validates with kuery', () => {
@ -434,7 +462,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", language: Invalid enum value. Expected 'kuery' | 'lucene', received 'something-made-up', type: Invalid literal value, expected \\"saved_query\\", saved_id: Required, and 19 more"`
);
});
test('max_signals cannot be negative', () => {
@ -493,7 +523,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"tags.0: Expected string, received number, tags.1: Expected string, received number, tags.2: Expected string, received number, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", and 38 more"`
);
});
test('You cannot send in an array of threat that are missing "framework"', () => {
@ -519,7 +551,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"threat.0.framework: Required, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", threat.0.framework: Required, threat.0.framework: Required, and 22 more"`
);
});
test('You cannot send in an array of threat that are missing "tactic"', () => {
@ -541,7 +575,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"threat.0.tactic: Required, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", threat.0.tactic: Required, threat.0.tactic: Required, and 22 more"`
);
});
test('You can send in an array of threat that are missing "technique"', () => {
@ -583,7 +619,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"false_positives.0: Expected string, received number, false_positives.1: Expected string, received number, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", false_positives.0: Expected string, received number, and 30 more"`
);
});
test('You cannot set the risk_score to 101', () => {
@ -655,7 +693,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"meta: Expected object, received string, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", meta: Expected object, received string, meta: Expected object, received string, and 22 more"`
);
});
test('You can omit the query string when filters are present', () => {
@ -690,7 +730,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'junk', type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'junk', severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'junk', and 22 more"`
);
});
test('You cannot send in an array of actions that are missing "group"', () => {
@ -701,7 +743,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"actions.0.group: Required, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", actions.0.group: Required, actions.0.group: Required, and 22 more"`
);
});
test('You cannot send in an array of actions that are missing "id"', () => {
@ -712,7 +756,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"actions.0.id: Required, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", actions.0.id: Required, actions.0.id: Required, and 22 more"`
);
});
test('You cannot send in an array of actions that are missing "action_type_id"', () => {
@ -723,7 +769,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"actions.0.action_type_id: Required, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", actions.0.action_type_id: Required, actions.0.action_type_id: Required, and 22 more"`
);
});
test('You cannot send in an array of actions that are missing "params"', () => {
@ -734,7 +782,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"actions.0.params: Required, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", actions.0.params: Required, actions.0.params: Required, and 22 more"`
);
});
test('You cannot send in an array of actions that are including "actionTypeId"', () => {
@ -752,7 +802,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"actions.0.action_type_id: Required, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", actions.0.action_type_id: Required, actions.0.action_type_id: Required, and 22 more"`
);
});
describe('note', () => {
@ -788,7 +840,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"note: Expected string, received object, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", note: Expected string, received object, note: Expected string, received object, and 22 more"`
);
});
test('empty name is not valid', () => {
@ -872,7 +926,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", type: Invalid literal value, expected \\"query\\", saved_id: Required, type: Invalid literal value, expected \\"threshold\\", and 14 more"`
);
});
test('threshold is required when type is threshold and will not validate without it', () => {
@ -880,7 +936,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", type: Invalid literal value, expected \\"query\\", type: Invalid literal value, expected \\"saved_query\\", saved_id: Required, and 14 more"`
);
});
test('threshold rules fail validation if threshold is not greater than 0', () => {
@ -958,7 +1016,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"exceptions_list.0.list_id: Required, exceptions_list.0.type: Required, exceptions_list.0.namespace_type: Invalid enum value. Expected 'agnostic' | 'single', received 'not a namespace type', type: Invalid literal value, expected \\"eql\\", query: Required, and 43 more"`
);
});
test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {
@ -999,7 +1059,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", type: Invalid literal value, expected \\"query\\", type: Invalid literal value, expected \\"saved_query\\", saved_id: Required, and 14 more"`
);
});
test('fails validation when threat_mapping is an empty array', () => {
@ -1068,7 +1130,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", data_view_id: Expected string, received number, data_view_id: Expected string, received number, type: Invalid literal value, expected \\"saved_query\\", and 20 more"`
);
});
test('it should validate a type of "query" with "data_view_id" defined', () => {
@ -1131,7 +1195,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"investigation_fields.field_names: Required, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", investigation_fields.field_names: Required, investigation_fields.field_names: Required, and 22 more"`
);
});
test('You can send in investigation_fields', () => {
@ -1166,7 +1232,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"investigation_fields.field_names.0: Expected string, received number, investigation_fields.field_names.1: Expected string, received number, investigation_fields.field_names.2: Expected string, received number, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", and 38 more"`
);
});
test('You cannot send in investigation_fields without specifying fields', () => {
@ -1177,7 +1245,9 @@ describe('rules schema', () => {
const result = RuleCreateProps.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"investigation_fields.field_names: Required, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", investigation_fields.field_names: Required, investigation_fields.field_names: Required, and 22 more"`
);
});
});
});

View file

@ -40,7 +40,9 @@ describe('Rule response schema', () => {
const result = RuleResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", type: Invalid literal value, expected \\"query\\", type: Invalid literal value, expected \\"saved_query\\", saved_id: Required, and 15 more"`
);
});
test('it should validate a type of "query" with a saved_id together', () => {
@ -68,7 +70,9 @@ describe('Rule response schema', () => {
const result = RuleResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", type: Invalid literal value, expected \\"query\\", saved_id: Required, type: Invalid literal value, expected \\"threshold\\", and 14 more"`
);
});
test('it should validate a type of "timeline_id" if there is a "timeline_title" dependent', () => {
@ -98,7 +102,9 @@ describe('Rule response schema', () => {
const result = RuleResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"exceptions_list: Expected array, received string, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", exceptions_list: Expected array, received string, exceptions_list: Expected array, received string, and 22 more"`
);
});
});
@ -232,6 +238,8 @@ describe('investigation_fields', () => {
const result = RuleResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('Invalid input');
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"investigation_fields: Expected object, received string, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", investigation_fields: Expected object, received string, investigation_fields: Expected object, received string, and 22 more"`
);
});
});

View file

@ -4,18 +4,9 @@
* 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 } {
@ -47,13 +38,13 @@ export const OsqueryParamsCamelCase = t.type({
// 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),
actionTypeId: t.literal('.osquery'),
params: OsqueryParamsCamelCase,
});
export type RuleResponseEndpointAction = t.TypeOf<typeof RuleResponseEndpointAction>;
export const RuleResponseEndpointAction = t.strict({
actionTypeId: t.literal(RESPONSE_ACTION_TYPES.ENDPOINT),
actionTypeId: t.literal('.endpoint'),
params: EndpointParams,
});
@ -67,12 +58,12 @@ export const ResponseActionRuleParamsOrUndefined = t.union([
// 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),
action_type_id: t.literal('.osquery'),
params: OsqueryParams,
});
const EndpointResponseAction = t.strict({
action_type_id: t.literal(RESPONSE_ACTION_TYPES.ENDPOINT),
action_type_id: t.literal('.endpoint'),
params: EndpointParams,
});

View file

@ -26,20 +26,10 @@ import {
threat_mapping,
threat_query,
} from '@kbn/securitysolution-io-ts-alerting-types';
import { PositiveInteger } from '@kbn/securitysolution-io-ts-types';
import { ResponseActionArray } from './response_actions';
import { RuleExecutionSummary } from '../../rule_monitoring/model';
// eslint-disable-next-line no-restricted-imports
import { ResponseActionArray } from '../rule_response_actions/response_actions_legacy';
import {
anomaly_threshold,
created_at,
created_by,
revision,
saved_id,
updated_at,
updated_by,
} from '../schemas';
import { anomaly_threshold, saved_id } from '../schemas';
import {
AlertsIndex,
@ -51,10 +41,7 @@ import {
InvestigationFields,
InvestigationGuide,
IsRuleEnabled,
IsRuleImmutable,
MaxSignals,
RelatedIntegrationArray,
RequiredFieldArray,
RuleAuthorArray,
RuleDescription,
RuleFalsePositiveArray,
@ -63,7 +50,6 @@ import {
RuleMetadata,
RuleName,
RuleNameOverride,
RuleObjectId,
RuleQuery,
RuleReferenceArray,
RuleSignatureId,
@ -72,7 +58,6 @@ import {
SavedObjectResolveAliasPurpose,
SavedObjectResolveAliasTargetId,
SavedObjectResolveOutcome,
SetupGuide,
ThreatArray,
TimelineTemplateId,
TimelineTemplateTitle,
@ -186,28 +171,24 @@ export const baseSchema = buildRuleSchemas({
},
});
const responseRequiredFields = {
id: RuleObjectId,
rule_id: RuleSignatureId,
immutable: IsRuleImmutable,
updated_at,
updated_by,
created_at,
created_by,
revision,
export type DurationMetric = t.TypeOf<typeof DurationMetric>;
export const DurationMetric = PositiveInteger;
// NOTE: For now, Related Integrations, Required Fields and Setup Guide are supported for prebuilt
// rules only. We don't want to allow users to edit these 3 fields via the API. If we added them
// to baseParams.defaultable, they would become a part of the request schema as optional fields.
// This is why we add them here, in order to add them only to the response schema.
related_integrations: RelatedIntegrationArray,
required_fields: RequiredFieldArray,
setup: SetupGuide,
};
export type RuleExecutionMetrics = t.TypeOf<typeof RuleExecutionMetrics>;
const responseOptionalFields = {
execution_summary: RuleExecutionSummary,
};
/**
@property total_search_duration_ms - "total time spent performing ES searches as measured by Kibana;
includes network latency and time spent serializing/deserializing request/response",
@property total_indexing_duration_ms - "total time spent indexing documents during current rule execution cycle",
@property total_enrichment_duration_ms - total time spent enriching documents during current rule execution cycle
@property execution_gap_duration_s - "duration in seconds of execution gap"
*/
export const RuleExecutionMetrics = t.partial({
total_search_duration_ms: DurationMetric,
total_indexing_duration_ms: DurationMetric,
total_enrichment_duration_ms: DurationMetric,
execution_gap_duration_s: DurationMetric,
});
export type BaseCreateProps = t.TypeOf<typeof BaseCreateProps>;
export const BaseCreateProps = baseSchema.create;
@ -225,36 +206,9 @@ export const SharedCreateProps = t.intersection([
t.exact(t.partial({ rule_id: RuleSignatureId })),
]);
type SharedUpdateProps = t.TypeOf<typeof SharedUpdateProps>;
const SharedUpdateProps = t.intersection([
baseSchema.create,
t.exact(t.partial({ rule_id: RuleSignatureId })),
t.exact(t.partial({ id: RuleObjectId })),
]);
type SharedPatchProps = t.TypeOf<typeof SharedPatchProps>;
const SharedPatchProps = t.intersection([
baseSchema.patch,
t.exact(t.partial({ rule_id: RuleSignatureId, id: RuleObjectId })),
]);
export type SharedResponseProps = t.TypeOf<typeof SharedResponseProps>;
export const SharedResponseProps = t.intersection([
baseSchema.response,
t.exact(t.type(responseRequiredFields)),
t.exact(t.partial(responseOptionalFields)),
]);
// -------------------------------------------------------------------------------------------------
// EQL rule schema
export enum QueryLanguage {
'kuery' = 'kuery',
'lucene' = 'lucene',
'eql' = 'eql',
'esql' = 'esql',
}
export type KqlQueryLanguage = t.TypeOf<typeof KqlQueryLanguage>;
export const KqlQueryLanguage = t.keyof({ kuery: null, lucene: null });
@ -278,21 +232,6 @@ const eqlSchema = buildRuleSchemas({
defaultable: {},
});
export type EqlRule = t.TypeOf<typeof EqlRule>;
export const EqlRule = t.intersection([SharedResponseProps, eqlSchema.response]);
export type EqlRuleCreateProps = t.TypeOf<typeof EqlRuleCreateProps>;
export const EqlRuleCreateProps = t.intersection([SharedCreateProps, eqlSchema.create]);
export type EqlRuleUpdateProps = t.TypeOf<typeof EqlRuleUpdateProps>;
export const EqlRuleUpdateProps = t.intersection([SharedUpdateProps, eqlSchema.create]);
export type EqlRulePatchProps = t.TypeOf<typeof EqlRulePatchProps>;
export const EqlRulePatchProps = t.intersection([SharedPatchProps, eqlSchema.patch]);
export type EqlPatchParams = t.TypeOf<typeof EqlPatchParams>;
export const EqlPatchParams = eqlSchema.patch;
// -------------------------------------------------------------------------------------------------
// ES|QL rule schema
@ -309,21 +248,6 @@ const esqlSchema = buildRuleSchemas({
defaultable: {},
});
export type EsqlRule = t.TypeOf<typeof EsqlRule>;
export const EsqlRule = t.intersection([SharedResponseProps, esqlSchema.response]);
export type EsqlRuleCreateProps = t.TypeOf<typeof EsqlRuleCreateProps>;
export const EsqlRuleCreateProps = t.intersection([SharedCreateProps, esqlSchema.create]);
export type EsqlRuleUpdateProps = t.TypeOf<typeof EsqlRuleUpdateProps>;
export const EsqlRuleUpdateProps = t.intersection([SharedUpdateProps, esqlSchema.create]);
export type EsqlRulePatchProps = t.TypeOf<typeof EsqlRulePatchProps>;
export const EsqlRulePatchProps = t.intersection([SharedPatchProps, esqlSchema.patch]);
export type EsqlPatchParams = t.TypeOf<typeof EsqlPatchParams>;
export const EsqlPatchParams = esqlSchema.patch;
// -------------------------------------------------------------------------------------------------
// Indicator Match rule schema
@ -351,30 +275,6 @@ const threatMatchSchema = buildRuleSchemas({
},
});
export type ThreatMatchRule = t.TypeOf<typeof ThreatMatchRule>;
export const ThreatMatchRule = t.intersection([SharedResponseProps, threatMatchSchema.response]);
export type ThreatMatchRuleCreateProps = t.TypeOf<typeof ThreatMatchRuleCreateProps>;
export const ThreatMatchRuleCreateProps = t.intersection([
SharedCreateProps,
threatMatchSchema.create,
]);
export type ThreatMatchRuleUpdateProps = t.TypeOf<typeof ThreatMatchRuleUpdateProps>;
export const ThreatMatchRuleUpdateProps = t.intersection([
SharedUpdateProps,
threatMatchSchema.create,
]);
export type ThreatMatchRulePatchProps = t.TypeOf<typeof ThreatMatchRulePatchProps>;
export const ThreatMatchRulePatchProps = t.intersection([
SharedPatchProps,
threatMatchSchema.patch,
]);
export type ThreatMatchPatchParams = t.TypeOf<typeof ThreatMatchPatchParams>;
export const ThreatMatchPatchParams = threatMatchSchema.patch;
// -------------------------------------------------------------------------------------------------
// Custom Query rule schema
@ -396,21 +296,6 @@ const querySchema = buildRuleSchemas({
},
});
export type QueryRule = t.TypeOf<typeof QueryRule>;
export const QueryRule = t.intersection([SharedResponseProps, querySchema.response]);
export type QueryRuleCreateProps = t.TypeOf<typeof QueryRuleCreateProps>;
export const QueryRuleCreateProps = t.intersection([SharedCreateProps, querySchema.create]);
export type QueryRuleUpdateProps = t.TypeOf<typeof QueryRuleUpdateProps>;
export const QueryRuleUpdateProps = t.intersection([SharedUpdateProps, querySchema.create]);
export type QueryRulePatchProps = t.TypeOf<typeof QueryRulePatchProps>;
export const QueryRulePatchProps = t.intersection([SharedPatchProps, querySchema.patch]);
export type QueryPatchParams = t.TypeOf<typeof QueryPatchParams>;
export const QueryPatchParams = querySchema.patch;
// -------------------------------------------------------------------------------------------------
// Saved Query rule schema
@ -434,27 +319,6 @@ const savedQuerySchema = buildRuleSchemas({
},
});
export type SavedQueryRule = t.TypeOf<typeof SavedQueryRule>;
export const SavedQueryRule = t.intersection([SharedResponseProps, savedQuerySchema.response]);
export type SavedQueryRuleCreateProps = t.TypeOf<typeof SavedQueryRuleCreateProps>;
export const SavedQueryRuleCreateProps = t.intersection([
SharedCreateProps,
savedQuerySchema.create,
]);
export type SavedQueryRuleUpdateProps = t.TypeOf<typeof SavedQueryRuleUpdateProps>;
export const SavedQueryRuleUpdateProps = t.intersection([
SharedUpdateProps,
savedQuerySchema.create,
]);
export type SavedQueryRulePatchProps = t.TypeOf<typeof SavedQueryRulePatchProps>;
export const SavedQueryRulePatchProps = t.intersection([SharedPatchProps, savedQuerySchema.patch]);
export type SavedQueryPatchParams = t.TypeOf<typeof SavedQueryPatchParams>;
export const SavedQueryPatchParams = savedQuerySchema.patch;
// -------------------------------------------------------------------------------------------------
// Threshold rule schema
@ -475,21 +339,6 @@ const thresholdSchema = buildRuleSchemas({
},
});
export type ThresholdRule = t.TypeOf<typeof ThresholdRule>;
export const ThresholdRule = t.intersection([SharedResponseProps, thresholdSchema.response]);
export type ThresholdRuleCreateProps = t.TypeOf<typeof ThresholdRuleCreateProps>;
export const ThresholdRuleCreateProps = t.intersection([SharedCreateProps, thresholdSchema.create]);
export type ThresholdRuleUpdateProps = t.TypeOf<typeof ThresholdRuleUpdateProps>;
export const ThresholdRuleUpdateProps = t.intersection([SharedUpdateProps, thresholdSchema.create]);
export type ThresholdRulePatchProps = t.TypeOf<typeof ThresholdRulePatchProps>;
export const ThresholdRulePatchProps = t.intersection([SharedPatchProps, thresholdSchema.patch]);
export type ThresholdPatchParams = t.TypeOf<typeof ThresholdPatchParams>;
export const ThresholdPatchParams = thresholdSchema.patch;
// -------------------------------------------------------------------------------------------------
// Machine Learning rule schema
@ -503,33 +352,6 @@ const machineLearningSchema = buildRuleSchemas({
defaultable: {},
});
export type MachineLearningRule = t.TypeOf<typeof MachineLearningRule>;
export const MachineLearningRule = t.intersection([
SharedResponseProps,
machineLearningSchema.response,
]);
export type MachineLearningRuleCreateProps = t.TypeOf<typeof MachineLearningRuleCreateProps>;
export const MachineLearningRuleCreateProps = t.intersection([
SharedCreateProps,
machineLearningSchema.create,
]);
export type MachineLearningRuleUpdateProps = t.TypeOf<typeof MachineLearningRuleUpdateProps>;
export const MachineLearningRuleUpdateProps = t.intersection([
SharedUpdateProps,
machineLearningSchema.create,
]);
export type MachineLearningRulePatchProps = t.TypeOf<typeof MachineLearningRulePatchProps>;
export const MachineLearningRulePatchProps = t.intersection([
SharedPatchProps,
machineLearningSchema.patch,
]);
export type MachineLearningPatchParams = t.TypeOf<typeof MachineLearningPatchParams>;
export const MachineLearningPatchParams = machineLearningSchema.patch;
// -------------------------------------------------------------------------------------------------
// New Terms rule schema
@ -550,21 +372,6 @@ const newTermsSchema = buildRuleSchemas({
},
});
export type NewTermsRule = t.TypeOf<typeof NewTermsRule>;
export const NewTermsRule = t.intersection([SharedResponseProps, newTermsSchema.response]);
export type NewTermsRuleCreateProps = t.TypeOf<typeof NewTermsRuleCreateProps>;
export const NewTermsRuleCreateProps = t.intersection([SharedCreateProps, newTermsSchema.create]);
export type NewTermsRuleUpdateProps = t.TypeOf<typeof NewTermsRuleUpdateProps>;
export const NewTermsRuleUpdateProps = t.intersection([SharedUpdateProps, newTermsSchema.create]);
export type NewTermsRulePatchProps = t.TypeOf<typeof NewTermsRulePatchProps>;
export const NewTermsRulePatchProps = t.intersection([SharedPatchProps, newTermsSchema.patch]);
export type NewTermsPatchParams = t.TypeOf<typeof NewTermsPatchParams>;
export const NewTermsPatchParams = newTermsSchema.patch;
// -------------------------------------------------------------------------------------------------
// Combined type specific schemas
@ -579,42 +386,3 @@ export const TypeSpecificCreateProps = t.union([
machineLearningSchema.create,
newTermsSchema.create,
]);
export type TypeSpecificPatchProps = t.TypeOf<typeof TypeSpecificPatchProps>;
export const TypeSpecificPatchProps = t.union([
eqlSchema.patch,
esqlSchema.patch,
threatMatchSchema.patch,
querySchema.patch,
savedQuerySchema.patch,
thresholdSchema.patch,
machineLearningSchema.patch,
newTermsSchema.patch,
]);
export type TypeSpecificResponse = t.TypeOf<typeof TypeSpecificResponse>;
export const TypeSpecificResponse = t.union([
eqlSchema.response,
esqlSchema.response,
threatMatchSchema.response,
querySchema.response,
savedQuerySchema.response,
thresholdSchema.response,
machineLearningSchema.response,
newTermsSchema.response,
]);
// -------------------------------------------------------------------------------------------------
// Final combined schemas
export type RuleCreateProps = t.TypeOf<typeof RuleCreateProps>;
export const RuleCreateProps = t.intersection([TypeSpecificCreateProps, SharedCreateProps]);
export type RuleUpdateProps = t.TypeOf<typeof RuleUpdateProps>;
export const RuleUpdateProps = t.intersection([TypeSpecificCreateProps, SharedUpdateProps]);
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([TypeSpecificResponse, SharedResponseProps]);

View file

@ -33,12 +33,6 @@ export type Status = t.TypeOf<typeof status>;
export const conflicts = t.keyof({ abort: null, proceed: null });
export const queryFilter = t.string;
export type QueryFilter = t.TypeOf<typeof queryFilter>;
export const queryFilterOrUndefined = t.union([queryFilter, t.undefined]);
export type QueryFilterOrUndefined = t.TypeOf<typeof queryFilterOrUndefined>;
export const signal_ids = t.array(t.string);
export type SignalIds = t.TypeOf<typeof signal_ids>;
@ -48,23 +42,12 @@ export const signal_status_query = t.object;
export const alert_tag_ids = t.array(t.string);
export type AlertTagIds = t.TypeOf<typeof alert_tag_ids>;
export const fields = t.array(t.string);
export type Fields = t.TypeOf<typeof fields>;
export const fieldsOrUndefined = t.union([fields, t.undefined]);
export type FieldsOrUndefined = t.TypeOf<typeof fieldsOrUndefined>;
export const created_at = IsoDateString;
export const updated_at = IsoDateString;
export const created_by = t.string;
export const updated_by = t.string;
export const status_code = PositiveInteger;
export const message = t.string;
export const perPage = PositiveInteger;
export const total = PositiveInteger;
export const revision = PositiveInteger;
export const success = t.boolean;
export const success_count = PositiveInteger;
export const indexRecord = t.record(
t.string,

View file

@ -5,85 +5,30 @@
* 2.0.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
import { DefaultSortOrderAsc, DefaultSortOrderDesc } from './sorting_legacy';
import { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import { SortOrder } from './sorting.gen';
describe('Common sorting schemas', () => {
describe('DefaultSortOrderAsc', () => {
describe('Validation succeeds', () => {
it('when valid sort order is passed', () => {
const payload = 'desc';
const decoded = DefaultSortOrderAsc.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
});
describe('Validation fails', () => {
it('when invalid sort order is passed', () => {
const payload = 'behind_you';
const decoded = DefaultSortOrderAsc.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "behind_you" supplied to "DefaultSortOrderAsc"',
]);
expect(message.schema).toEqual({});
});
});
describe('Validation sets the default sort order "asc"', () => {
it('when sort order is not passed', () => {
const payload = undefined;
const decoded = DefaultSortOrderAsc.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('asc');
});
});
describe('SortOrder schema', () => {
it('accepts asc value', () => {
const payload = 'asc';
const result = SortOrder.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
describe('DefaultSortOrderDesc', () => {
describe('Validation succeeds', () => {
it('when valid sort order is passed', () => {
const payload = 'asc';
const decoded = DefaultSortOrderDesc.decode(payload);
const message = pipe(decoded, foldLeftRight);
it('accepts desc value', () => {
const payload = 'desc';
const result = SortOrder.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
});
describe('Validation fails', () => {
it('when invalid sort order is passed', () => {
const payload = 'behind_you';
const decoded = DefaultSortOrderDesc.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "behind_you" supplied to "DefaultSortOrderDesc"',
]);
expect(message.schema).toEqual({});
});
});
describe('Validation sets the default sort order "desc"', () => {
it('when sort order is not passed', () => {
const payload = null;
const decoded = DefaultSortOrderDesc.decode(payload);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual('desc');
});
});
it('fails on unknown value', () => {
const payload = 'invalid';
const result = SortOrder.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual(
"Invalid enum value. Expected 'asc' | 'desc', received 'invalid'"
);
});
});

View file

@ -1,40 +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 type { Either } from 'fp-ts/lib/Either';
import { capitalize } from 'lodash';
export type SortOrder = t.TypeOf<typeof SortOrder>;
export const SortOrder = t.keyof({ asc: null, desc: null });
export type SortOrderOrUndefined = t.TypeOf<typeof SortOrderOrUndefined>;
export const SortOrderOrUndefined = t.union([SortOrder, t.undefined]);
const defaultSortOrder = (order: SortOrder): t.Type<SortOrder, SortOrder, unknown> => {
return new t.Type<SortOrder, SortOrder, unknown>(
`DefaultSortOrder${capitalize(order)}`,
SortOrder.is,
(input, context): Either<t.Errors, SortOrder> =>
input == null ? t.success(order) : SortOrder.validate(input, context),
t.identity
);
};
/**
* Types the DefaultSortOrderAsc as:
* - If undefined, then a default sort order of 'asc' will be set
* - If a string is sent in, then the string will be validated to ensure it's a valid SortOrder
*/
export const DefaultSortOrderAsc = defaultSortOrder('asc');
/**
* Types the DefaultSortOrderDesc as:
* - If undefined, then a default sort order of 'desc' will be set
* - If a string is sent in, then the string will be validated to ensure it's a valid SortOrder
*/
export const DefaultSortOrderDesc = defaultSortOrder('desc');

View file

@ -12,9 +12,7 @@ import type {
ExceptionListItemSchema,
} from '@kbn/securitysolution-io-ts-list-types';
import { createRuleExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
import { RuleObjectId } from '../../model/rule_schema_legacy';
import { UUID } from '@kbn/securitysolution-io-ts-types';
/**
* URL path parameters of the API route.
@ -22,7 +20,7 @@ import { RuleObjectId } from '../../model/rule_schema_legacy';
export type CreateRuleExceptionsRequestParams = t.TypeOf<typeof CreateRuleExceptionsRequestParams>;
export const CreateRuleExceptionsRequestParams = t.exact(
t.type({
id: RuleObjectId,
id: UUID,
})
);

View file

@ -0,0 +1,291 @@
/*
* 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 { BooleanFromString } from '@kbn/zod-helpers';
/*
* 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 {
RuleActionGroup,
RuleActionId,
RuleActionParams,
RuleActionFrequency,
RuleActionAlertsFilter,
IndexPatternArray,
RuleTagArray,
TimelineTemplateId,
TimelineTemplateTitle,
} from '../../model/rule_schema/common_attributes.gen';
export type BulkEditSkipReason = z.infer<typeof BulkEditSkipReason>;
export const BulkEditSkipReason = z.literal('RULE_NOT_MODIFIED');
export type BulkActionSkipResult = z.infer<typeof BulkActionSkipResult>;
export const BulkActionSkipResult = z.object({
id: z.string(),
name: z.string().optional(),
skip_reason: BulkEditSkipReason,
});
export type RuleDetailsInError = z.infer<typeof RuleDetailsInError>;
export const RuleDetailsInError = z.object({
id: z.string(),
name: z.string().optional(),
});
export type BulkActionsDryRunErrCode = z.infer<typeof BulkActionsDryRunErrCode>;
export const BulkActionsDryRunErrCode = z.enum([
'IMMUTABLE',
'MACHINE_LEARNING_AUTH',
'MACHINE_LEARNING_INDEX_PATTERN',
'ESQL_INDEX_PATTERN',
]);
export type BulkActionsDryRunErrCodeEnum = typeof BulkActionsDryRunErrCode.enum;
export const BulkActionsDryRunErrCodeEnum = BulkActionsDryRunErrCode.enum;
export type NormalizedRuleError = z.infer<typeof NormalizedRuleError>;
export const NormalizedRuleError = z.object({
message: z.string(),
status_code: z.number().int(),
err_code: BulkActionsDryRunErrCode.optional(),
rules: z.array(RuleDetailsInError),
});
export type BulkEditActionResults = z.infer<typeof BulkEditActionResults>;
export const BulkEditActionResults = z.object({
updated: z.array(RuleResponse),
created: z.array(RuleResponse),
deleted: z.array(RuleResponse),
skipped: z.array(BulkActionSkipResult),
});
export type BulkEditActionSummary = z.infer<typeof BulkEditActionSummary>;
export const BulkEditActionSummary = z.object({
failed: z.number().int(),
skipped: z.number().int(),
succeeded: z.number().int(),
total: z.number().int(),
});
export type BulkEditActionResponse = z.infer<typeof BulkEditActionResponse>;
export const BulkEditActionResponse = z.object({
success: z.boolean().optional(),
status_code: z.number().int().optional(),
message: z.string().optional(),
rules_count: z.number().int().optional(),
attributes: z.object({
results: BulkEditActionResults,
summary: BulkEditActionSummary,
errors: z.array(NormalizedRuleError).optional(),
}),
});
export type BulkExportActionResponse = z.infer<typeof BulkExportActionResponse>;
export const BulkExportActionResponse = z.string();
export type BulkActionBase = z.infer<typeof BulkActionBase>;
export const BulkActionBase = z.object({
/**
* Query to filter rules
*/
query: z.string().optional(),
/**
* Array of rule IDs
*/
ids: z.array(z.string()).min(1).optional(),
});
export type BulkDeleteRules = z.infer<typeof BulkDeleteRules>;
export const BulkDeleteRules = BulkActionBase.and(
z.object({
action: z.literal('delete'),
})
);
export type BulkDisableRules = z.infer<typeof BulkDisableRules>;
export const BulkDisableRules = BulkActionBase.and(
z.object({
action: z.literal('disable'),
})
);
export type BulkEnableRules = z.infer<typeof BulkEnableRules>;
export const BulkEnableRules = BulkActionBase.and(
z.object({
action: z.literal('enable'),
})
);
export type BulkExportRules = z.infer<typeof BulkExportRules>;
export const BulkExportRules = BulkActionBase.and(
z.object({
action: z.literal('export'),
})
);
export type BulkDuplicateRules = z.infer<typeof BulkDuplicateRules>;
export const BulkDuplicateRules = BulkActionBase.and(
z.object({
action: z.literal('duplicate'),
duplicate: z
.object({
/**
* Whether to copy exceptions from the original rule
*/
include_exceptions: z.boolean(),
/**
* Whether to copy expired exceptions from the original rule
*/
include_expired_exceptions: z.boolean(),
})
.optional(),
})
);
/**
* The condition for throttling the notification: 'rule', 'no_actions', or time duration
*/
export type ThrottleForBulkActions = z.infer<typeof ThrottleForBulkActions>;
export const ThrottleForBulkActions = z.enum(['rule', '1h', '1d', '7d']);
export type ThrottleForBulkActionsEnum = typeof ThrottleForBulkActions.enum;
export const ThrottleForBulkActionsEnum = ThrottleForBulkActions.enum;
export type BulkActionType = z.infer<typeof BulkActionType>;
export const BulkActionType = z.enum([
'enable',
'disable',
'export',
'delete',
'duplicate',
'edit',
]);
export type BulkActionTypeEnum = typeof BulkActionType.enum;
export const BulkActionTypeEnum = BulkActionType.enum;
export type BulkActionEditType = z.infer<typeof BulkActionEditType>;
export const BulkActionEditType = z.enum([
'add_tags',
'delete_tags',
'set_tags',
'add_index_patterns',
'delete_index_patterns',
'set_index_patterns',
'set_timeline',
'add_rule_actions',
'set_rule_actions',
'set_schedule',
]);
export type BulkActionEditTypeEnum = typeof BulkActionEditType.enum;
export const BulkActionEditTypeEnum = BulkActionEditType.enum;
export type NormalizedRuleAction = z.infer<typeof NormalizedRuleAction>;
export const NormalizedRuleAction = z
.object({
group: RuleActionGroup,
id: RuleActionId,
params: RuleActionParams,
frequency: RuleActionFrequency.optional(),
alerts_filter: RuleActionAlertsFilter.optional(),
})
.strict();
export type BulkActionEditPayloadRuleActions = z.infer<typeof BulkActionEditPayloadRuleActions>;
export const BulkActionEditPayloadRuleActions = z.object({
type: z.enum(['add_rule_actions', 'set_rule_actions']),
value: z.object({
throttle: ThrottleForBulkActions.optional(),
actions: z.array(NormalizedRuleAction),
}),
});
export type BulkActionEditPayloadSchedule = z.infer<typeof BulkActionEditPayloadSchedule>;
export const BulkActionEditPayloadSchedule = z.object({
type: z.literal('set_schedule'),
value: z.object({
/**
* Interval in which the rule is executed
*/
interval: z.string().regex(/^[1-9]\d*[smh]$/),
/**
* Lookback time for the rule
*/
lookback: z.string().regex(/^[1-9]\d*[smh]$/),
}),
});
export type BulkActionEditPayloadIndexPatterns = z.infer<typeof BulkActionEditPayloadIndexPatterns>;
export const BulkActionEditPayloadIndexPatterns = z.object({
type: z.enum(['add_index_patterns', 'delete_index_patterns', 'set_index_patterns']),
value: IndexPatternArray,
overwrite_data_views: z.boolean().optional(),
});
export type BulkActionEditPayloadTags = z.infer<typeof BulkActionEditPayloadTags>;
export const BulkActionEditPayloadTags = z.object({
type: z.enum(['add_tags', 'delete_tags', 'set_tags']),
value: RuleTagArray,
});
export type BulkActionEditPayloadTimeline = z.infer<typeof BulkActionEditPayloadTimeline>;
export const BulkActionEditPayloadTimeline = z.object({
type: z.literal('set_timeline'),
value: z.object({
timeline_id: TimelineTemplateId,
timeline_title: TimelineTemplateTitle,
}),
});
export type BulkActionEditPayload = z.infer<typeof BulkActionEditPayload>;
export const BulkActionEditPayload = z.union([
BulkActionEditPayloadTags,
BulkActionEditPayloadIndexPatterns,
BulkActionEditPayloadTimeline,
BulkActionEditPayloadRuleActions,
BulkActionEditPayloadSchedule,
]);
export type BulkEditRules = z.infer<typeof BulkEditRules>;
export const BulkEditRules = BulkActionBase.and(
z.object({
action: z.literal('edit'),
/**
* Array of objects containing the edit operations
*/
edit: z.array(BulkActionEditPayload).min(1),
})
);
export type PerformBulkActionRequestQuery = z.infer<typeof PerformBulkActionRequestQuery>;
export const PerformBulkActionRequestQuery = z.object({
/**
* Enables dry run mode for the request call.
*/
dry_run: BooleanFromString.optional(),
});
export type PerformBulkActionRequestQueryInput = z.input<typeof PerformBulkActionRequestQuery>;
export type PerformBulkActionRequestBody = z.infer<typeof PerformBulkActionRequestBody>;
export const PerformBulkActionRequestBody = z.union([
BulkDeleteRules,
BulkDisableRules,
BulkEnableRules,
BulkExportRules,
BulkDuplicateRules,
BulkEditRules,
]);
export type PerformBulkActionRequestBodyInput = z.input<typeof PerformBulkActionRequestBody>;
export type PerformBulkActionResponse = z.infer<typeof PerformBulkActionResponse>;
export const PerformBulkActionResponse = z.union([
BulkEditActionResponse,
BulkExportActionResponse,
]);

View file

@ -5,18 +5,18 @@
* 2.0.
*/
import { BulkActionType, BulkActionEditType } from './bulk_actions_route';
import type { PerformBulkActionRequestBody } from './bulk_actions_route';
import type { PerformBulkActionRequestBody } from './bulk_actions_route.gen';
import { BulkActionEditTypeEnum, BulkActionTypeEnum } from './bulk_actions_route.gen';
export const getPerformBulkActionSchemaMock = (): PerformBulkActionRequestBody => ({
query: '',
ids: undefined,
action: BulkActionType.disable,
action: BulkActionTypeEnum.disable,
});
export const getPerformBulkActionEditSchemaMock = (): PerformBulkActionRequestBody => ({
query: '',
ids: undefined,
action: BulkActionType.edit,
[BulkActionType.edit]: [{ type: BulkActionEditType.add_tags, value: ['tag1'] }],
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [{ type: BulkActionEditTypeEnum.add_tags, value: ['tag1'] }],
});

View file

@ -6,7 +6,7 @@ paths:
/api/detection_engine/rules/_bulk_action:
post:
operationId: PerformBulkAction
x-codegen-enabled: false
x-codegen-enabled: true
summary: Applies a bulk action to multiple rules
description: The bulk action is applied to all rules that match the filter or to the list of rules by their IDs.
tags:
@ -22,19 +22,24 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/PerformBulkActionRequest'
oneOf:
- $ref: '#/components/schemas/BulkDeleteRules'
- $ref: '#/components/schemas/BulkDisableRules'
- $ref: '#/components/schemas/BulkEnableRules'
- $ref: '#/components/schemas/BulkExportRules'
- $ref: '#/components/schemas/BulkDuplicateRules'
- $ref: '#/components/schemas/BulkEditRules'
responses:
200:
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/BulkEditActionResponse'
oneOf:
- $ref: '#/components/schemas/BulkEditActionResponse'
- $ref: '#/components/schemas/BulkExportActionResponse'
components:
x-codegen-enabled: false
schemas:
BulkEditSkipReason:
type: string
@ -66,6 +71,11 @@ components:
BulkActionsDryRunErrCode:
type: string
enum:
- IMMUTABLE
- MACHINE_LEARNING_AUTH
- MACHINE_LEARNING_INDEX_PATTERN
- ESQL_INDEX_PATTERN
NormalizedRuleError:
type: object
@ -127,35 +137,17 @@ components:
- succeeded
- total
BulkEditActionSuccessResponse:
BulkEditActionResponse:
type: object
properties:
success:
type: boolean
rules_count:
type: integer
attributes:
type: object
properties:
results:
$ref: '#/components/schemas/BulkEditActionResults'
summary:
$ref: '#/components/schemas/BulkEditActionSummary'
required:
- results
- summary
required:
- success
- rules_count
- attributes
BulkEditActionErrorResponse:
type: object
properties:
status_code:
type: integer
message:
type: string
rules_count:
type: integer
attributes:
type: object
properties:
@ -171,35 +163,23 @@ components:
- results
- summary
required:
- status_code
- message
- attributes
BulkEditActionResponse:
oneOf:
- $ref: '#/components/schemas/BulkEditActionSuccessResponse'
- $ref: '#/components/schemas/BulkEditActionErrorResponse'
BulkExportActionResponse:
type: string
BulkActionBase:
oneOf:
- type: object
properties:
query:
type: string
description: Query to filter rules
required:
- query
additionalProperties: false
- type: object
properties:
ids:
type: array
description: Array of rule IDs
minItems: 1
items:
type: string
additionalProperties: false
type: object
properties:
query:
type: string
description: Query to filter rules
ids:
type: array
description: Array of rule IDs
minItems: 1
items:
type: string
BulkDeleteRules:
allOf:
@ -262,35 +242,20 @@ components:
include_expired_exceptions:
type: boolean
description: Whether to copy expired exceptions from the original rule
required:
- include_exceptions
- include_expired_exceptions
required:
- action
RuleActionSummary:
type: boolean
description: Action summary indicates whether we will send a summary notification about all the generate alerts or notification per individual alert
RuleActionNotifyWhen:
type: string
description: "The condition for throttling the notification: 'onActionGroupChange', 'onActiveAlert', or 'onThrottleInterval'"
enum:
- onActionGroupChange
- onActiveAlert
- onThrottleInterval
RuleActionThrottle:
ThrottleForBulkActions:
type: string
description: "The condition for throttling the notification: 'rule', 'no_actions', or time duration"
RuleActionFrequency:
type: object
properties:
summary:
$ref: '#/components/schemas/RuleActionSummary'
notifyWhen:
$ref: '#/components/schemas/RuleActionNotifyWhen'
throttle:
$ref: '#/components/schemas/RuleActionThrottle'
nullable: true
enum:
- rule
- 1h
- 1d
- 7d
BulkActionType:
type: string
@ -316,6 +281,26 @@ components:
- set_rule_actions
- set_schedule
# Per rulesClient.bulkEdit rules actions operation contract (x-pack/plugins/alerting/server/rules_client/rules_client.ts) normalized rule action object is expected (NormalizedAlertAction) as value for the edit operation
NormalizedRuleAction:
type: object
properties:
group:
$ref: '../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/RuleActionGroup'
id:
$ref: '../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/RuleActionId'
params:
$ref: '../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/RuleActionParams'
frequency:
$ref: '../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/RuleActionFrequency'
alerts_filter:
$ref: '../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/RuleActionAlertsFilter'
required:
- group
- id
- params
additionalProperties: false
BulkActionEditPayloadRuleActions:
type: object
properties:
@ -326,28 +311,11 @@ components:
type: object
properties:
throttle:
$ref: '#/components/schemas/RuleActionThrottle'
$ref: '#/components/schemas/ThrottleForBulkActions'
actions:
type: array
items:
type: object
properties:
group:
type: string
description: Action group
id:
type: string
description: Action ID
params:
type: object
description: Action parameters
frequency:
$ref: '#/components/schemas/RuleActionFrequency'
description: Action frequency
required:
- group
- id
- params
$ref: '#/components/schemas/NormalizedRuleAction'
required:
- actions
required:
@ -366,12 +334,19 @@ components:
interval:
type: string
description: Interval in which the rule is executed
pattern: '^[1-9]\d*[smh]$' # any number except zero followed by one of the suffixes 's', 'm', 'h'
example: '1h'
lookback:
type: string
description: Lookback time for the rule
pattern: '^[1-9]\d*[smh]$' # any number except zero followed by one of the suffixes 's', 'm', 'h'
example: '1h'
required:
- interval
- lookback
required:
- type
- value
BulkActionEditPayloadIndexPatterns:
type: object
@ -441,22 +416,13 @@ components:
properties:
action:
type: string
x-type: literal
enum: [edit]
edit:
type: array
description: Array of objects containing the edit operations
items:
$ref: '#/components/schemas/BulkActionEditPayload'
minItems: 1
required:
- action
- rule
PerformBulkActionRequest:
oneOf:
- $ref: '#/components/schemas/BulkDeleteRules'
- $ref: '#/components/schemas/BulkDisableRules'
- $ref: '#/components/schemas/BulkEnableRules'
- $ref: '#/components/schemas/BulkExportRules'
- $ref: '#/components/schemas/BulkDuplicateRules'
- $ref: '#/components/schemas/BulkEditRules'
- edit

View file

@ -5,19 +5,12 @@
* 2.0.
*/
import { left } from 'fp-ts/lib/Either';
import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
import { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import {
BulkActionEditTypeEnum,
BulkActionTypeEnum,
PerformBulkActionRequestBody,
BulkActionType,
BulkActionEditType,
} from './bulk_actions_route';
const retrieveValidationMessage = (payload: unknown) => {
const decoded = PerformBulkActionRequestBody.decode(payload);
const checked = exactCheck(payload, decoded);
return foldLeftRight(checked);
};
} from './bulk_actions_route.gen';
describe('Perform bulk action request schema', () => {
describe('cases common to every bulk action', () => {
@ -25,62 +18,64 @@ describe('Perform bulk action request schema', () => {
test('valid request: missing query', () => {
const payload: PerformBulkActionRequestBody = {
query: undefined,
action: BulkActionType.enable,
action: BulkActionTypeEnum.enable,
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('invalid request: missing action', () => {
const payload: Omit<PerformBulkActionRequestBody, 'action'> = {
query: 'name: test',
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expectParseError(result);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "undefined" supplied to "action"',
'Invalid value "undefined" supplied to "edit"',
]);
expect(message.schema).toEqual({});
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 2 more"`
);
});
test('invalid request: unknown action', () => {
const payload: Omit<PerformBulkActionRequestBody, 'action'> & { action: 'unknown' } = {
query: 'name: test',
action: 'unknown',
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expectParseError(result);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "unknown" supplied to "action"',
'Invalid value "undefined" supplied to "edit"',
]);
expect(message.schema).toEqual({});
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 2 more"`
);
});
test('invalid request: unknown property', () => {
test('strips unknown properties', () => {
const payload = {
query: 'name: test',
action: BulkActionType.enable,
action: BulkActionTypeEnum.enable,
mock: ['id'],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "mock,["id"]"']);
expect(message.schema).toEqual({});
expect(result.data).toEqual({
query: 'name: test',
action: BulkActionTypeEnum.enable,
});
});
test('invalid request: wrong type for ids', () => {
const payload = {
ids: 'mock',
action: BulkActionType.enable,
action: BulkActionTypeEnum.enable,
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expectParseError(result);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "mock" supplied to "ids"']);
expect(message.schema).toEqual({});
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"ids: Expected array, received string, action: Invalid literal value, expected \\"delete\\", ids: Expected array, received string, action: Invalid literal value, expected \\"disable\\", ids: Expected array, received string, and 7 more"`
);
});
});
@ -88,11 +83,11 @@ describe('Perform bulk action request schema', () => {
test('valid request', () => {
const payload: PerformBulkActionRequestBody = {
query: 'name: test',
action: BulkActionType.enable,
action: BulkActionTypeEnum.enable,
};
const message = retrieveValidationMessage(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
});
@ -100,11 +95,11 @@ describe('Perform bulk action request schema', () => {
test('valid request', () => {
const payload: PerformBulkActionRequestBody = {
query: 'name: test',
action: BulkActionType.disable,
action: BulkActionTypeEnum.disable,
};
const message = retrieveValidationMessage(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
});
@ -112,11 +107,11 @@ describe('Perform bulk action request schema', () => {
test('valid request', () => {
const payload: PerformBulkActionRequestBody = {
query: 'name: test',
action: BulkActionType.export,
action: BulkActionTypeEnum.export,
};
const message = retrieveValidationMessage(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
});
@ -124,11 +119,11 @@ describe('Perform bulk action request schema', () => {
test('valid request', () => {
const payload: PerformBulkActionRequestBody = {
query: 'name: test',
action: BulkActionType.delete,
action: BulkActionTypeEnum.delete,
};
const message = retrieveValidationMessage(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
});
@ -136,15 +131,15 @@ describe('Perform bulk action request schema', () => {
test('valid request', () => {
const payload: PerformBulkActionRequestBody = {
query: 'name: test',
action: BulkActionType.duplicate,
[BulkActionType.duplicate]: {
action: BulkActionTypeEnum.duplicate,
[BulkActionTypeEnum.duplicate]: {
include_exceptions: false,
include_expired_exceptions: false,
},
};
const message = retrieveValidationMessage(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
});
@ -153,47 +148,30 @@ describe('Perform bulk action request schema', () => {
test('invalid request: missing edit payload', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
action: BulkActionTypeEnum.edit,
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expectParseError(result);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "edit" supplied to "action"',
'Invalid value "undefined" supplied to "edit"',
]);
expect(message.schema).toEqual({});
});
test('invalid request: specified edit payload for another action', () => {
const payload = {
query: 'name: test',
action: BulkActionType.enable,
[BulkActionType.edit]: [{ type: BulkActionEditType.set_tags, value: ['test-tag'] }],
};
const message = retrieveValidationMessage(payload);
expect(getPaths(left(message.errors))).toEqual([
'invalid keys "edit,[{"type":"set_tags","value":["test-tag"]}]"',
]);
expect(message.schema).toEqual({});
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 1 more"`
);
});
test('invalid request: wrong type for edit payload', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: { type: BulkActionEditType.set_tags, value: ['test-tag'] },
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: { type: BulkActionEditTypeEnum.set_tags, value: ['test-tag'] },
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expectParseError(result);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "edit" supplied to "action"',
'Invalid value "{"type":"set_tags","value":["test-tag"]}" supplied to "edit"',
]);
expect(message.schema).toEqual({});
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 1 more"`
);
});
});
@ -201,57 +179,61 @@ describe('Perform bulk action request schema', () => {
test('invalid request: wrong tags type', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [{ type: BulkActionEditType.set_tags, value: 'test-tag' }],
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [{ type: BulkActionEditTypeEnum.set_tags, value: 'test-tag' }],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expectParseError(result);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "edit" supplied to "action"',
'Invalid value "test-tag" supplied to "edit,value"',
'Invalid value "set_tags" supplied to "edit,type"',
]);
expect(message.schema).toEqual({});
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 9 more"`
);
});
test('valid request: add_tags edit action', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [{ type: BulkActionEditType.add_tags, value: ['test-tag'] }],
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{ type: BulkActionEditTypeEnum.add_tags, value: ['test-tag'] },
],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('valid request: set_tags edit action', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [{ type: BulkActionEditType.set_tags, value: ['test-tag'] }],
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{ type: BulkActionEditTypeEnum.set_tags, value: ['test-tag'] },
],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('valid request: delete_tags edit action', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [{ type: BulkActionEditType.delete_tags, value: ['test-tag'] }],
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{ type: BulkActionEditTypeEnum.delete_tags, value: ['test-tag'] },
],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
});
@ -259,63 +241,61 @@ describe('Perform bulk action request schema', () => {
test('invalid request: wrong index_patterns type', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [{ type: BulkActionEditType.set_tags, value: 'logs-*' }],
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [{ type: BulkActionEditTypeEnum.set_tags, value: 'logs-*' }],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expectParseError(result);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "edit" supplied to "action"',
'Invalid value "logs-*" supplied to "edit,value"',
'Invalid value "set_tags" supplied to "edit,type"',
]);
expect(message.schema).toEqual({});
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 9 more"`
);
});
test('valid request: set_index_patterns edit action', () => {
const payload: PerformBulkActionRequestBody = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [
{ type: BulkActionEditType.set_index_patterns, value: ['logs-*'] },
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{ type: BulkActionEditTypeEnum.set_index_patterns, value: ['logs-*'] },
],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('valid request: add_index_patterns edit action', () => {
const payload: PerformBulkActionRequestBody = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [
{ type: BulkActionEditType.add_index_patterns, value: ['logs-*'] },
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{ type: BulkActionEditTypeEnum.add_index_patterns, value: ['logs-*'] },
],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('valid request: delete_index_patterns edit action', () => {
const payload: PerformBulkActionRequestBody = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [
{ type: BulkActionEditType.delete_index_patterns, value: ['logs-*'] },
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{ type: BulkActionEditTypeEnum.delete_index_patterns, value: ['logs-*'] },
],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
});
@ -323,27 +303,25 @@ describe('Perform bulk action request schema', () => {
test('invalid request: wrong timeline payload type', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [{ type: BulkActionEditType.set_timeline, value: [] }],
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [{ type: BulkActionEditTypeEnum.set_timeline, value: [] }],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "edit" supplied to "action"',
'Invalid value "set_timeline" supplied to "edit,type"',
'Invalid value "[]" supplied to "edit,value"',
]);
expect(message.schema).toEqual({});
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 7 more"`
);
});
test('invalid request: missing timeline_id', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{
type: BulkActionEditType.set_timeline,
type: BulkActionEditTypeEnum.set_timeline,
value: {
timeline_title: 'Test timeline title',
},
@ -351,24 +329,21 @@ describe('Perform bulk action request schema', () => {
],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual(
expect.arrayContaining([
'Invalid value "{"timeline_title":"Test timeline title"}" supplied to "edit,value"',
'Invalid value "undefined" supplied to "edit,value,timeline_id"',
])
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 10 more"`
);
expect(message.schema).toEqual({});
});
test('valid request: set_timeline edit action', () => {
const payload: PerformBulkActionRequestBody = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{
type: BulkActionEditType.set_timeline,
type: BulkActionEditTypeEnum.set_timeline,
value: {
timeline_id: 'timelineid',
timeline_title: 'Test timeline title',
@ -377,10 +352,10 @@ describe('Perform bulk action request schema', () => {
],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
});
@ -388,27 +363,25 @@ describe('Perform bulk action request schema', () => {
test('invalid request: wrong schedules payload type', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [{ type: BulkActionEditType.set_schedule, value: [] }],
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [{ type: BulkActionEditTypeEnum.set_schedule, value: [] }],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "edit" supplied to "action"',
'Invalid value "set_schedule" supplied to "edit,type"',
'Invalid value "[]" supplied to "edit,value"',
]);
expect(message.schema).toEqual({});
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 7 more"`
);
});
test('invalid request: wrong type of payload data', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{
type: BulkActionEditType.set_schedule,
type: BulkActionEditTypeEnum.set_schedule,
value: {
interval: '-10m',
lookback: '1m',
@ -417,25 +390,21 @@ describe('Perform bulk action request schema', () => {
],
} as PerformBulkActionRequestBody;
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual(
expect.arrayContaining([
'Invalid value "edit" supplied to "action"',
'Invalid value "{"interval":"-10m","lookback":"1m"}" supplied to "edit,value"',
'Invalid value "-10m" supplied to "edit,value,interval"',
])
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"edit.0.value.interval: Invalid"`
);
expect(message.schema).toEqual({});
});
test('invalid request: missing interval', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{
type: BulkActionEditType.set_schedule,
type: BulkActionEditTypeEnum.set_schedule,
value: {
lookback: '1m',
},
@ -443,25 +412,21 @@ describe('Perform bulk action request schema', () => {
],
} as PerformBulkActionRequestBody;
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual(
expect.arrayContaining([
'Invalid value "edit" supplied to "action"',
'Invalid value "{"lookback":"1m"}" supplied to "edit,value"',
'Invalid value "undefined" supplied to "edit,value,interval"',
])
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 10 more"`
);
expect(message.schema).toEqual({});
});
test('invalid request: missing lookback', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{
type: BulkActionEditType.set_schedule,
type: BulkActionEditTypeEnum.set_schedule,
value: {
interval: '1m',
},
@ -469,25 +434,21 @@ describe('Perform bulk action request schema', () => {
],
} as PerformBulkActionRequestBody;
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual(
expect.arrayContaining([
'Invalid value "edit" supplied to "action"',
'Invalid value "{"interval":"1m"}" supplied to "edit,value"',
'Invalid value "undefined" supplied to "edit,value,lookback"',
])
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 10 more"`
);
expect(message.schema).toEqual({});
});
test('valid request: set_schedule edit action', () => {
const payload: PerformBulkActionRequestBody = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{
type: BulkActionEditType.set_schedule,
type: BulkActionEditTypeEnum.set_schedule,
value: {
interval: '1m',
lookback: '1m',
@ -496,10 +457,10 @@ describe('Perform bulk action request schema', () => {
],
} as PerformBulkActionRequestBody;
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
});
@ -507,25 +468,25 @@ describe('Perform bulk action request schema', () => {
test('invalid request: invalid rule actions payload', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [{ type: BulkActionEditType.add_rule_actions, value: [] }],
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [{ type: BulkActionEditTypeEnum.add_rule_actions, value: [] }],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual(
expect.arrayContaining(['Invalid value "[]" supplied to "edit,value"'])
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 7 more"`
);
expect(message.schema).toEqual({});
});
test('invalid request: missing actions in payload', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{
type: BulkActionEditType.add_rule_actions,
type: BulkActionEditTypeEnum.add_rule_actions,
value: {
throttle: '1h',
},
@ -533,21 +494,21 @@ describe('Perform bulk action request schema', () => {
],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual(
expect.arrayContaining(['Invalid value "undefined" supplied to "edit,value,actions"'])
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 11 more"`
);
expect(message.schema).toEqual({});
});
test('invalid request: invalid action_type_id property in actions array', () => {
const payload = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{
type: BulkActionEditType.add_rule_actions,
type: BulkActionEditTypeEnum.add_rule_actions,
value: {
throttle: '1h',
actions: [
@ -567,20 +528,20 @@ describe('Perform bulk action request schema', () => {
],
};
const message = retrieveValidationMessage(payload);
expect(getPaths(left(message.errors))).toEqual(
expect.arrayContaining(['invalid keys "action_type_id"'])
const result = PerformBulkActionRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"edit.0.value.actions.0: Unrecognized key(s) in object: 'action_type_id'"`
);
expect(message.schema).toEqual({});
});
test('valid request: add_rule_actions edit action', () => {
const payload: PerformBulkActionRequestBody = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{
type: BulkActionEditType.add_rule_actions,
type: BulkActionEditTypeEnum.add_rule_actions,
value: {
throttle: '1h',
actions: [
@ -599,19 +560,19 @@ describe('Perform bulk action request schema', () => {
],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('valid request: set_rule_actions edit action', () => {
const payload: PerformBulkActionRequestBody = {
query: 'name: test',
action: BulkActionType.edit,
[BulkActionType.edit]: [
action: BulkActionTypeEnum.edit,
[BulkActionTypeEnum.edit]: [
{
type: BulkActionEditType.set_rule_actions,
type: BulkActionEditTypeEnum.set_rule_actions,
value: {
throttle: '1h',
actions: [
@ -632,10 +593,10 @@ describe('Perform bulk action request schema', () => {
],
};
const message = retrieveValidationMessage(payload);
const result = PerformBulkActionRequestBody.safeParse(payload);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
});
});

View file

@ -1,269 +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 { NonEmptyArray, TimeDuration } from '@kbn/securitysolution-io-ts-types';
import {
RuleActionAlertsFilter,
RuleActionFrequency,
RuleActionGroup,
RuleActionId,
RuleActionParams,
} from '@kbn/securitysolution-io-ts-alerting-types';
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/rule_schema_legacy';
export enum BulkActionType {
'enable' = 'enable',
'disable' = 'disable',
'export' = 'export',
'delete' = 'delete',
'duplicate' = 'duplicate',
'edit' = 'edit',
}
export enum BulkActionEditType {
'add_tags' = 'add_tags',
'delete_tags' = 'delete_tags',
'set_tags' = 'set_tags',
'add_index_patterns' = 'add_index_patterns',
'delete_index_patterns' = 'delete_index_patterns',
'set_index_patterns' = 'set_index_patterns',
'set_timeline' = 'set_timeline',
'add_rule_actions' = 'add_rule_actions',
'set_rule_actions' = 'set_rule_actions',
'set_schedule' = 'set_schedule',
}
export type ThrottleForBulkActions = t.TypeOf<typeof ThrottleForBulkActions>;
export const ThrottleForBulkActions = t.union([
t.literal('rule'),
t.literal('1h'),
t.literal('1d'),
t.literal('7d'),
]);
type BulkActionEditPayloadTags = t.TypeOf<typeof BulkActionEditPayloadTags>;
const BulkActionEditPayloadTags = t.type({
type: t.union([
t.literal(BulkActionEditType.add_tags),
t.literal(BulkActionEditType.delete_tags),
t.literal(BulkActionEditType.set_tags),
]),
value: RuleTagArray,
});
export type BulkActionEditPayloadIndexPatterns = t.TypeOf<
typeof BulkActionEditPayloadIndexPatterns
>;
const BulkActionEditPayloadIndexPatterns = t.intersection([
t.type({
type: t.union([
t.literal(BulkActionEditType.add_index_patterns),
t.literal(BulkActionEditType.delete_index_patterns),
t.literal(BulkActionEditType.set_index_patterns),
]),
value: IndexPatternArray,
}),
t.exact(t.partial({ overwrite_data_views: t.boolean })),
]);
type BulkActionEditPayloadTimeline = t.TypeOf<typeof BulkActionEditPayloadTimeline>;
const BulkActionEditPayloadTimeline = t.type({
type: t.literal(BulkActionEditType.set_timeline),
value: t.type({
timeline_id: TimelineTemplateId,
timeline_title: TimelineTemplateTitle,
}),
});
/**
* per rulesClient.bulkEdit rules actions operation contract (x-pack/plugins/alerting/server/rules_client/rules_client.ts)
* normalized rule action object is expected (NormalizedAlertAction) as value for the edit operation
*/
export type NormalizedRuleAction = t.TypeOf<typeof NormalizedRuleAction>;
export const NormalizedRuleAction = t.exact(
t.intersection([
t.type({
group: RuleActionGroup,
id: RuleActionId,
params: RuleActionParams,
}),
t.partial({ frequency: RuleActionFrequency }),
t.partial({ alerts_filter: RuleActionAlertsFilter }),
])
);
export type BulkActionEditPayloadRuleActions = t.TypeOf<typeof BulkActionEditPayloadRuleActions>;
export const BulkActionEditPayloadRuleActions = t.type({
type: t.union([
t.literal(BulkActionEditType.add_rule_actions),
t.literal(BulkActionEditType.set_rule_actions),
]),
value: t.intersection([
t.partial({ throttle: ThrottleForBulkActions }),
t.type({
actions: t.array(NormalizedRuleAction),
}),
]),
});
type BulkActionEditPayloadSchedule = t.TypeOf<typeof BulkActionEditPayloadSchedule>;
const BulkActionEditPayloadSchedule = t.type({
type: t.literal(BulkActionEditType.set_schedule),
value: t.type({
interval: TimeDuration({ allowedUnits: ['s', 'm', 'h'] }),
lookback: TimeDuration({ allowedUnits: ['s', 'm', 'h'] }),
}),
});
export type BulkActionEditPayload = t.TypeOf<typeof BulkActionEditPayload>;
export const BulkActionEditPayload = t.union([
BulkActionEditPayloadTags,
BulkActionEditPayloadIndexPatterns,
BulkActionEditPayloadTimeline,
BulkActionEditPayloadRuleActions,
BulkActionEditPayloadSchedule,
]);
const bulkActionDuplicatePayload = t.exact(
t.type({
include_exceptions: t.boolean,
include_expired_exceptions: t.boolean,
})
);
export type BulkActionDuplicatePayload = t.TypeOf<typeof bulkActionDuplicatePayload>;
/**
* actions that modify rules attributes
*/
export type BulkActionEditForRuleAttributes =
| BulkActionEditPayloadTags
| BulkActionEditPayloadRuleActions
| BulkActionEditPayloadSchedule;
/**
* actions that modify rules params
*/
export type BulkActionEditForRuleParams =
| BulkActionEditPayloadIndexPatterns
| BulkActionEditPayloadTimeline
| BulkActionEditPayloadSchedule;
/**
* Request body parameters of the API route.
*/
export type PerformBulkActionRequestBody = t.TypeOf<typeof PerformBulkActionRequestBody>;
export const PerformBulkActionRequestBody = t.intersection([
t.exact(
t.type({
query: t.union([RuleQuery, t.undefined]),
})
),
t.exact(t.partial({ ids: NonEmptyArray(t.string) })),
t.union([
t.exact(
t.type({
action: t.union([
t.literal(BulkActionType.delete),
t.literal(BulkActionType.disable),
t.literal(BulkActionType.enable),
t.literal(BulkActionType.export),
]),
})
),
t.intersection([
t.exact(
t.type({
action: t.literal(BulkActionType.duplicate),
})
),
t.exact(
t.partial({
[BulkActionType.duplicate]: bulkActionDuplicatePayload,
})
),
]),
t.exact(
t.type({
action: t.literal(BulkActionType.edit),
[BulkActionType.edit]: NonEmptyArray(BulkActionEditPayload),
})
),
]),
]);
/**
* Query string parameters of the API route.
*/
export type PerformBulkActionRequestQuery = t.TypeOf<typeof PerformBulkActionRequestQuery>;
export const PerformBulkActionRequestQuery = t.exact(
t.partial({
dry_run: t.union([t.literal('true'), t.literal('false')]),
})
);
export interface RuleDetailsInError {
id: string;
name?: string;
}
export interface NormalizedRuleError {
message: string;
status_code: number;
err_code?: BulkActionsDryRunErrCode;
rules: RuleDetailsInError[];
}
export interface BulkEditActionResults {
updated: RuleResponse[];
created: RuleResponse[];
deleted: RuleResponse[];
skipped: BulkActionSkipResult[];
}
export interface BulkEditActionSummary {
failed: number;
skipped: number;
succeeded: number;
total: number;
}
export interface BulkEditActionSuccessResponse {
success: boolean;
rules_count: number;
attributes: {
results: BulkEditActionResults;
summary: BulkEditActionSummary;
};
}
export interface BulkEditActionErrorResponse {
status_code: number;
message: string;
attributes: {
results: BulkEditActionResults;
summary: BulkEditActionSummary;
errors?: NormalizedRuleError[];
};
}
export type BulkEditActionResponse = BulkEditActionSuccessResponse | BulkEditActionErrorResponse;
export type BulkExportActionResponse = string;
export type PerformBulkActionResponse = BulkEditActionResponse | BulkExportActionResponse;

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type {
BulkActionEditPayloadIndexPatterns,
BulkActionEditPayloadRuleActions,
BulkActionEditPayloadSchedule,
BulkActionEditPayloadTags,
BulkActionEditPayloadTimeline,
} from './bulk_actions_route.gen';
/**
* actions that modify rules attributes
*/
export type BulkActionEditForRuleAttributes =
| BulkActionEditPayloadTags
| BulkActionEditPayloadRuleActions
| BulkActionEditPayloadSchedule;
/**
* actions that modify rules params
*/
export type BulkActionEditForRuleParams =
| BulkActionEditPayloadIndexPatterns
| BulkActionEditPayloadTimeline
| BulkActionEditPayloadSchedule;

View file

@ -26,7 +26,9 @@ describe('Bulk create rules request schema', () => {
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.name: Required, 0.description: Required, 0.risk_score: Required, 0.severity: Required, 0.type: Invalid literal value, expected \\"eql\\", and 52 more"`
);
});
test('single array element does validate', () => {
@ -56,7 +58,9 @@ describe('Bulk create rules request schema', () => {
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.risk_score: Required, 0.type: Invalid literal value, expected \\"eql\\", 0.language: Invalid literal value, expected \\"eql\\", 0.risk_score: Required, 0.risk_score: Required, and 22 more"`
);
});
test('two array elements where the first is valid but the second is invalid (risk_score) will not validate', () => {
@ -68,7 +72,9 @@ describe('Bulk create rules request schema', () => {
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"1: Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"1.risk_score: Required, 1.type: Invalid literal value, expected \\"eql\\", 1.language: Invalid literal value, expected \\"eql\\", 1.risk_score: Required, 1.risk_score: Required, and 22 more"`
);
});
test('two array elements where the first is invalid (risk_score) but the second is valid will not validate', () => {
@ -80,7 +86,9 @@ describe('Bulk create rules request schema', () => {
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.risk_score: Required, 0.type: Invalid literal value, expected \\"eql\\", 0.language: Invalid literal value, expected \\"eql\\", 0.risk_score: Required, 0.risk_score: Required, and 22 more"`
);
});
test('two array elements where both are invalid (risk_score) will not validate', () => {
@ -95,7 +103,7 @@ describe('Bulk create rules request schema', () => {
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0: Invalid input, 1: Invalid input"`
`"0.risk_score: Required, 0.type: Invalid literal value, expected \\"eql\\", 0.language: Invalid literal value, expected \\"eql\\", 0.risk_score: Required, 0.risk_score: Required, and 49 more"`
);
});
@ -121,7 +129,9 @@ describe('Bulk create rules request schema', () => {
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'madeup', 0.type: Invalid literal value, expected \\"eql\\", 0.language: Invalid literal value, expected \\"eql\\", 0.severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'madeup', 0.severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'madeup', and 22 more"`
);
});
test('You can set "note" to a string', () => {
@ -154,6 +164,8 @@ describe('Bulk create rules request schema', () => {
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.note: Expected string, received object, 0.type: Invalid literal value, expected \\"eql\\", 0.language: Invalid literal value, expected \\"eql\\", 0.note: Expected string, received object, 0.note: Expected string, received object, and 22 more"`
);
});
});

View file

@ -70,6 +70,8 @@ describe('Bulk patch rules request schema', () => {
const result = BulkPatchRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"1: Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"1.note: Expected string, received object, 1.note: Expected string, received object, 1.note: Expected string, received object, 1.note: Expected string, received object, 1.note: Expected string, received object, and 3 more"`
);
});
});

View file

@ -27,7 +27,9 @@ describe('Bulk update rules request schema', () => {
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.name: Required, 0.description: Required, 0.risk_score: Required, 0.severity: Required, 0.type: Invalid literal value, expected \\"eql\\", and 52 more"`
);
});
test('single array element does validate', () => {
@ -57,7 +59,9 @@ describe('Bulk update rules request schema', () => {
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.risk_score: Required, 0.type: Invalid literal value, expected \\"eql\\", 0.language: Invalid literal value, expected \\"eql\\", 0.risk_score: Required, 0.risk_score: Required, and 22 more"`
);
});
test('two array elements where the first is valid but the second is invalid (risk_score) will not validate', () => {
@ -69,7 +73,9 @@ describe('Bulk update rules request schema', () => {
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"1: Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"1.risk_score: Required, 1.type: Invalid literal value, expected \\"eql\\", 1.language: Invalid literal value, expected \\"eql\\", 1.risk_score: Required, 1.risk_score: Required, and 22 more"`
);
});
test('two array elements where the first is invalid (risk_score) but the second is valid will not validate', () => {
@ -81,7 +87,9 @@ describe('Bulk update rules request schema', () => {
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.risk_score: Required, 0.type: Invalid literal value, expected \\"eql\\", 0.language: Invalid literal value, expected \\"eql\\", 0.risk_score: Required, 0.risk_score: Required, and 22 more"`
);
});
test('two array elements where both are invalid (risk_score) will not validate', () => {
@ -96,7 +104,7 @@ describe('Bulk update rules request schema', () => {
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0: Invalid input, 1: Invalid input"`
`"0.risk_score: Required, 0.type: Invalid literal value, expected \\"eql\\", 0.language: Invalid literal value, expected \\"eql\\", 0.risk_score: Required, 0.risk_score: Required, and 49 more"`
);
});
@ -122,7 +130,9 @@ describe('Bulk update rules request schema', () => {
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'madeup', 0.type: Invalid literal value, expected \\"eql\\", 0.language: Invalid literal value, expected \\"eql\\", 0.severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'madeup', 0.severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'madeup', and 22 more"`
);
});
test('You can set "namespace" to a string', () => {
@ -165,6 +175,8 @@ describe('Bulk update rules request schema', () => {
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.note: Expected string, received object, 0.type: Invalid literal value, expected \\"eql\\", 0.language: Invalid literal value, expected \\"eql\\", 0.note: Expected string, received object, 0.note: Expected string, received object, and 22 more"`
);
});
});

View file

@ -45,7 +45,9 @@ describe('Bulk CRUD rules response schema', () => {
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.error: Required, 0: Unrecognized key(s) in object: 'author', 'created_at', 'updated_at', 'created_by', 'description', 'enabled', 'false_positives', 'from', 'immutable', 'references', 'revision', 'severity', 'severity_mapping', 'updated_by', 'tags', 'to', 'threat', 'version', 'output_index', 'max_signals', 'risk_score', 'risk_score_mapping', 'interval', 'exceptions_list', 'related_integrations', 'required_fields', 'setup', 'throttle', 'actions', 'building_block_type', 'note', 'license', 'outcome', 'alias_target_id', 'alias_purpose', 'timeline_id', 'timeline_title', 'meta', 'rule_name_override', 'timestamp_override', 'timestamp_override_fallback_disabled', 'namespace', 'investigation_fields', 'query', 'type', 'language', 'index', 'data_view_id', 'filters', 'saved_id', 'response_actions', 'alert_suppression', 0.name: Required, 0.type: Invalid literal value, expected \\"eql\\", 0.language: Invalid literal value, expected \\"eql\\", and 24 more"`
);
});
test('it should NOT validate an invalid error message with a deleted value', () => {
@ -56,7 +58,9 @@ describe('Bulk CRUD rules response schema', () => {
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0: Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.error: Required, 0.name: Required, 0.description: Required, 0.risk_score: Required, 0.severity: Required, and 267 more"`
);
});
test('it should omit any extra rule props', () => {

View file

@ -373,7 +373,9 @@ describe('Patch rule request schema', () => {
const result = PatchRuleRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"references.0: Expected string, received number, references.0: Expected string, received number, references.0: Expected string, received number, references.0: Expected string, received number, references.0: Expected string, received number, and 3 more"`
);
});
test('indexes cannot be numbers', () => {
@ -385,7 +387,9 @@ describe('Patch rule request schema', () => {
const result = PatchRuleRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"type: Invalid literal value, expected \\"eql\\", index.0: Expected string, received number, index.0: Expected string, received number, type: Invalid literal value, expected \\"saved_query\\", index.0: Expected string, received number, and 8 more"`
);
});
test('saved_id is not required when type is saved_query and will validate without it', () => {
@ -456,7 +460,9 @@ describe('Patch rule request schema', () => {
const result = PatchRuleRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", language: Invalid enum value. Expected 'kuery' | 'lucene', received 'something-made-up', type: Invalid literal value, expected \\"saved_query\\", language: Invalid enum value. Expected 'kuery' | 'lucene', received 'something-made-up', and 9 more"`
);
});
test('max_signals cannot be negative', () => {
@ -518,7 +524,9 @@ describe('Patch rule request schema', () => {
const result = PatchRuleRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"meta: Expected object, received string, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", meta: Expected object, received string, meta: Expected object, received string, and 12 more"`
);
});
test('filters cannot be a string', () => {
@ -529,7 +537,9 @@ describe('Patch rule request schema', () => {
const result = PatchRuleRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", filters: Expected array, received string, filters: Expected array, received string, type: Invalid literal value, expected \\"saved_query\\", and 10 more"`
);
});
test('name cannot be an empty string', () => {
@ -631,7 +641,9 @@ describe('Patch rule request schema', () => {
const result = PatchRuleRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"threat.0.framework: Required, threat.0.framework: Required, threat.0.framework: Required, threat.0.framework: Required, threat.0.framework: Required, and 3 more"`
);
});
test('threat is invalid when updated with missing tactic sub-object', () => {
@ -655,7 +667,9 @@ describe('Patch rule request schema', () => {
const result = PatchRuleRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"threat.0.tactic: Required, threat.0.tactic: Required, threat.0.tactic: Required, threat.0.tactic: Required, threat.0.tactic: Required, and 3 more"`
);
});
test('threat is valid when updated with missing technique', () => {
@ -700,7 +714,9 @@ describe('Patch rule request schema', () => {
const result = PatchRuleRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'junk', severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'junk', severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'junk', severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'junk', severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'junk', and 3 more"`
);
});
describe('note', () => {
@ -744,7 +760,9 @@ describe('Patch rule request schema', () => {
const result = PatchRuleRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"note: Expected string, received object, note: Expected string, received object, note: Expected string, received object, note: Expected string, received object, note: Expected string, received object, and 3 more"`
);
});
});
@ -756,7 +774,9 @@ describe('Patch rule request schema', () => {
const result = PatchRuleRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"actions.0.group: Required, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", actions.0.group: Required, actions.0.group: Required, and 12 more"`
);
});
test('You cannot send in an array of actions that are missing "id"', () => {
@ -767,7 +787,9 @@ describe('Patch rule request schema', () => {
const result = PatchRuleRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"actions.0.id: Required, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", actions.0.id: Required, actions.0.id: Required, and 12 more"`
);
});
test('You cannot send in an array of actions that are missing "params"', () => {
@ -778,7 +800,9 @@ describe('Patch rule request schema', () => {
const result = PatchRuleRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"actions.0.params: Required, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", actions.0.params: Required, actions.0.params: Required, and 12 more"`
);
});
test('You cannot send in an array of actions that are including "actionTypeId"', () => {
@ -796,7 +820,9 @@ describe('Patch rule request schema', () => {
const result = PatchRuleRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"actions.0.action_type_id: Required, type: Invalid literal value, expected \\"eql\\", language: Invalid literal value, expected \\"eql\\", actions.0.action_type_id: Required, actions.0.action_type_id: Required, and 12 more"`
);
});
describe('exception_list', () => {
@ -862,7 +888,9 @@ describe('Patch rule request schema', () => {
const result = PatchRuleRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"Invalid input"`);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"exceptions_list.0.list_id: Required, exceptions_list.0.type: Required, exceptions_list.0.namespace_type: Invalid enum value. Expected 'agnostic' | 'single', received 'not a namespace type', type: Invalid literal value, expected \\"eql\\", exceptions_list.0.list_id: Required, and 26 more"`
);
});
test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {

View file

@ -6,6 +6,7 @@
*/
import { z } from 'zod';
import { BooleanFromString } from '@kbn/zod-helpers';
/*
* NOTICE: Do not edit this file manually.
@ -19,13 +20,7 @@ export const ExportRulesRequestQuery = z.object({
/**
* Determines whether a summary of the exported rules is returned.
*/
exclude_export_details: z.preprocess(
(value: unknown) => (typeof value === 'boolean' ? String(value) : value),
z
.enum(['true', 'false'])
.default('false')
.transform((value) => value === 'true')
),
exclude_export_details: BooleanFromString.optional().default(false),
/**
* File name for saving the exported rules.
*/

View file

@ -120,7 +120,7 @@ describe('Export rules request schema', () => {
const result = ExportRulesRequestQuery.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual(
`exclude_export_details: Invalid enum value. Expected 'true' | 'false', received 'invalid string'`
`exclude_export_details: Invalid enum value. Expected 'true' | 'false', received 'invalid string', exclude_export_details: Expected boolean, received string`
);
});
});

View file

@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from 'zod';
import { ArrayFromString } from '@kbn/zod-helpers';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
import { SortOrder } from '../../model/sorting.gen';
import { RuleResponse } from '../../model/rule_schema/rule_schemas.gen';
export type FindRulesSortField = z.infer<typeof FindRulesSortField>;
export const FindRulesSortField = z.enum([
'created_at',
'createdAt',
'enabled',
'execution_summary.last_execution.date',
'execution_summary.last_execution.metrics.execution_gap_duration_s',
'execution_summary.last_execution.metrics.total_indexing_duration_ms',
'execution_summary.last_execution.metrics.total_search_duration_ms',
'execution_summary.last_execution.status',
'name',
'risk_score',
'riskScore',
'severity',
'updated_at',
'updatedAt',
]);
export type FindRulesSortFieldEnum = typeof FindRulesSortField.enum;
export const FindRulesSortFieldEnum = FindRulesSortField.enum;
export type FindRulesRequestQuery = z.infer<typeof FindRulesRequestQuery>;
export const FindRulesRequestQuery = z.object({
fields: ArrayFromString(z.string()).optional(),
/**
* Search query
*/
filter: z.string().optional(),
/**
* Field to sort by
*/
sort_field: FindRulesSortField.optional(),
/**
* Sort order
*/
sort_order: SortOrder.optional(),
/**
* Page number
*/
page: z.coerce.number().int().min(1).optional().default(1),
/**
* Rules per page
*/
per_page: z.coerce.number().int().min(0).optional().default(20),
});
export type FindRulesRequestQueryInput = z.input<typeof FindRulesRequestQuery>;
export type FindRulesResponse = z.infer<typeof FindRulesResponse>;
export const FindRulesResponse = z.object({
page: z.number().int(),
perPage: z.number().int(),
total: z.number().int(),
data: z.array(RuleResponse),
});

View file

@ -0,0 +1,98 @@
openapi: 3.0.0
info:
title: Find Rules API endpoint
version: 2023-10-31
paths:
/api/detection_engine/rules/_find:
get:
operationId: FindRules
x-codegen-enabled: true
description: Finds rules that match the given query.
tags:
- Rules API
parameters:
- name: 'fields'
in: query
required: false
schema:
type: array
items:
type: string
- name: 'filter'
in: query
description: Search query
required: false
schema:
type: string
- name: 'sort_field'
in: query
description: Field to sort by
required: false
schema:
$ref: '#/components/schemas/FindRulesSortField'
- name: 'sort_order'
in: query
description: Sort order
required: false
schema:
$ref: '../../model/sorting.schema.yaml#/components/schemas/SortOrder'
- name: 'page'
in: query
description: Page number
required: false
schema:
type: integer
minimum: 1
default: 1
- name: 'per_page'
in: query
description: Rules per page
required: false
schema:
type: integer
minimum: 0
default: 20
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
page:
type: integer
perPage:
type: integer
total:
type: integer
data:
type: array
items:
$ref: '../../model/rule_schema/rule_schemas.schema.yaml#/components/schemas/RuleResponse'
required:
- page
- perPage
- total
- data
components:
schemas:
FindRulesSortField:
type: string
enum:
- 'created_at'
- 'createdAt' # Legacy notation, keeping for backwards compatibility
- 'enabled'
- 'execution_summary.last_execution.date'
- 'execution_summary.last_execution.metrics.execution_gap_duration_s'
- 'execution_summary.last_execution.metrics.total_indexing_duration_ms'
- 'execution_summary.last_execution.metrics.total_search_duration_ms'
- 'execution_summary.last_execution.status'
- 'name'
- 'risk_score'
- 'riskScore' # Legacy notation, keeping for backwards compatibility
- 'severity'
- 'updated_at'
- 'updatedAt' # Legacy notation, keeping for backwards compatibility

View file

@ -5,21 +5,17 @@
* 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 { FindRulesRequestQuery } from './find_rules_route';
import { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import type { FindRulesRequestQueryInput } from './find_rules_route.gen';
import { FindRulesRequestQuery } from './find_rules_route.gen';
describe('Find rules request schema', () => {
test('empty objects do validate', () => {
const payload: FindRulesRequestQuery = {};
const payload: FindRulesRequestQueryInput = {};
const decoded = FindRulesRequestQuery.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({
const result = FindRulesRequestQuery.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual({
page: 1,
per_page: 20,
});
@ -35,167 +31,126 @@ describe('Find rules request schema', () => {
sort_order: 'asc',
};
const decoded = FindRulesRequestQuery.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 = FindRulesRequestQuery.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('made up parameters do not validate', () => {
const payload: Partial<FindRulesRequestQuery> & { madeUp: string } = {
test('made up parameters are ignored', () => {
const payload: Partial<FindRulesRequestQueryInput> & { madeUp: string } = {
madeUp: 'invalid value',
};
const decoded = FindRulesRequestQuery.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['invalid keys "madeUp"']);
expect(message.schema).toEqual({});
const result = FindRulesRequestQuery.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual({
page: 1,
per_page: 20,
});
});
test('per_page validates', () => {
const payload: FindRulesRequestQuery = {
const payload: FindRulesRequestQueryInput = {
per_page: 5,
};
const decoded = FindRulesRequestQuery.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesRequestQuery).per_page).toEqual(payload.per_page);
const result = FindRulesRequestQuery.safeParse(payload);
expectParseSuccess(result);
expect(result.data.per_page).toEqual(payload.per_page);
});
test('page validates', () => {
const payload: FindRulesRequestQuery = {
const payload: FindRulesRequestQueryInput = {
page: 5,
};
const decoded = FindRulesRequestQuery.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesRequestQuery).page).toEqual(payload.page);
const result = FindRulesRequestQuery.safeParse(payload);
expectParseSuccess(result);
expect(result.data.page).toEqual(payload.page);
});
test('sort_field validates', () => {
const payload: FindRulesRequestQuery = {
const payload: FindRulesRequestQueryInput = {
sort_field: 'name',
};
const decoded = FindRulesRequestQuery.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesRequestQuery).sort_field).toEqual('name');
const result = FindRulesRequestQuery.safeParse(payload);
expectParseSuccess(result);
expect(result.data.sort_field).toEqual(payload.sort_field);
});
test('fields validates with a string', () => {
const payload: FindRulesRequestQuery = {
const payload: FindRulesRequestQueryInput = {
fields: ['some value'],
};
const decoded = FindRulesRequestQuery.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesRequestQuery).fields).toEqual(payload.fields);
const result = FindRulesRequestQuery.safeParse(payload);
expectParseSuccess(result);
expect(result.data.fields).toEqual(payload.fields);
});
test('fields validates with multiple strings', () => {
const payload: FindRulesRequestQuery = {
const payload: FindRulesRequestQueryInput = {
fields: ['some value 1', 'some value 2'],
};
const decoded = FindRulesRequestQuery.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesRequestQuery).fields).toEqual(payload.fields);
const result = FindRulesRequestQuery.safeParse(payload);
expectParseSuccess(result);
expect(result.data.fields).toEqual(payload.fields);
});
test('fields does not validate with a number', () => {
const payload: Omit<FindRulesRequestQuery, 'fields'> & { fields: number } = {
const payload: Omit<FindRulesRequestQueryInput, 'fields'> & { fields: number } = {
fields: 5,
};
const decoded = FindRulesRequestQuery.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "fields"']);
expect(message.schema).toEqual({});
});
test('per_page has a default of 20', () => {
const payload: FindRulesRequestQuery = {};
const decoded = FindRulesRequestQuery.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesRequestQuery).per_page).toEqual(20);
});
test('page has a default of 1', () => {
const payload: FindRulesRequestQuery = {};
const decoded = FindRulesRequestQuery.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesRequestQuery).page).toEqual(1);
const result = FindRulesRequestQuery.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('fields: Expected array, received number');
});
test('filter works with a string', () => {
const payload: FindRulesRequestQuery = {
const payload: FindRulesRequestQueryInput = {
filter: 'some value 1',
};
const decoded = FindRulesRequestQuery.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesRequestQuery).filter).toEqual(payload.filter);
const result = FindRulesRequestQuery.safeParse(payload);
expectParseSuccess(result);
expect(result.data.filter).toEqual(payload.filter);
});
test('filter does not work with a number', () => {
const payload: Omit<FindRulesRequestQuery, 'filter'> & { filter: number } = {
const payload: Omit<FindRulesRequestQueryInput, 'filter'> & { filter: number } = {
filter: 5,
};
const decoded = FindRulesRequestQuery.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "filter"']);
expect(message.schema).toEqual({});
const result = FindRulesRequestQuery.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual('filter: Expected string, received number');
});
test('sort_order validates with desc and sort_field', () => {
const payload: FindRulesRequestQuery = {
const payload: FindRulesRequestQueryInput = {
sort_order: 'desc',
sort_field: 'name',
};
const decoded = FindRulesRequestQuery.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect((message.schema as FindRulesRequestQuery).sort_order).toEqual(payload.sort_order);
expect((message.schema as FindRulesRequestQuery).sort_field).toEqual(payload.sort_field);
const result = FindRulesRequestQuery.safeParse(payload);
expectParseSuccess(result);
expect(result.data.sort_order).toEqual(payload.sort_order);
expect(result.data.sort_field).toEqual(payload.sort_field);
});
test('sort_order does not validate with a string other than asc and desc', () => {
const payload: Omit<FindRulesRequestQuery, 'sort_order'> & { sort_order: string } = {
const payload: Omit<FindRulesRequestQueryInput, 'sort_order'> & { sort_order: string } = {
sort_order: 'some other string',
sort_field: 'name',
};
const decoded = FindRulesRequestQuery.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
'Invalid value "some other string" supplied to "sort_order"',
]);
expect(message.schema).toEqual({});
const result = FindRulesRequestQuery.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toEqual(
"sort_order: Invalid enum value. Expected 'asc' | 'desc', received 'some other string'"
);
});
});

View file

@ -1,54 +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 { DefaultPerPage, DefaultPage } from '@kbn/securitysolution-io-ts-alerting-types';
import type { RuleResponse } from '../../model';
import { SortOrder, queryFilter, fields } from '../../model';
export type FindRulesSortField = t.TypeOf<typeof FindRulesSortField>;
export const FindRulesSortField = t.union([
t.literal('created_at'),
t.literal('createdAt'), // Legacy notation, keeping for backwards compatibility
t.literal('enabled'),
t.literal('execution_summary.last_execution.date'),
t.literal('execution_summary.last_execution.metrics.execution_gap_duration_s'),
t.literal('execution_summary.last_execution.metrics.total_indexing_duration_ms'),
t.literal('execution_summary.last_execution.metrics.total_search_duration_ms'),
t.literal('execution_summary.last_execution.status'),
t.literal('name'),
t.literal('risk_score'),
t.literal('riskScore'), // Legacy notation, keeping for backwards compatibility
t.literal('severity'),
t.literal('updated_at'),
t.literal('updatedAt'), // Legacy notation, keeping for backwards compatibility
]);
export type FindRulesSortFieldOrUndefined = t.TypeOf<typeof FindRulesSortFieldOrUndefined>;
export const FindRulesSortFieldOrUndefined = t.union([FindRulesSortField, t.undefined]);
/**
* Query string parameters of the API route.
*/
export type FindRulesRequestQuery = t.TypeOf<typeof FindRulesRequestQuery>;
export const FindRulesRequestQuery = t.exact(
t.partial({
fields,
filter: queryFilter,
sort_field: FindRulesSortField,
sort_order: SortOrder,
page: DefaultPage, // defaults to 1
per_page: DefaultPerPage, // defaults to 20
})
);
export interface FindRulesResponse {
page: number;
perPage: number;
total: number;
data: RuleResponse[];
}

View file

@ -5,19 +5,19 @@
* 2.0.
*/
import type { FindRulesRequestQuery } from './find_rules_route';
import type { FindRulesRequestQueryInput } from './find_rules_route.gen';
import { validateFindRulesRequestQuery } from './request_schema_validation';
describe('Find rules request schema, additional validation', () => {
describe('validateFindRulesRequestQuery', () => {
test('You can have an empty sort_field and empty sort_order', () => {
const schema: FindRulesRequestQuery = {};
const schema: FindRulesRequestQueryInput = {};
const errors = validateFindRulesRequestQuery(schema);
expect(errors).toEqual([]);
});
test('You can have both a sort_field and and a sort_order', () => {
const schema: FindRulesRequestQuery = {
const schema: FindRulesRequestQueryInput = {
sort_field: 'name',
sort_order: 'asc',
};
@ -26,7 +26,7 @@ describe('Find rules request schema, additional validation', () => {
});
test('You cannot have sort_field without sort_order', () => {
const schema: FindRulesRequestQuery = {
const schema: FindRulesRequestQueryInput = {
sort_field: 'name',
};
const errors = validateFindRulesRequestQuery(schema);
@ -36,7 +36,7 @@ describe('Find rules request schema, additional validation', () => {
});
test('You cannot have sort_order without sort_field', () => {
const schema: FindRulesRequestQuery = {
const schema: FindRulesRequestQueryInput = {
sort_order: 'asc',
};
const errors = validateFindRulesRequestQuery(schema);

View file

@ -5,23 +5,16 @@
* 2.0.
*/
import type { FindRulesRequestQuery } from './find_rules_route';
import type { FindRulesRequestQueryInput } from './find_rules_route.gen';
/**
* Additional validation that is implemented outside of the schema itself.
*/
export const validateFindRulesRequestQuery = (query: FindRulesRequestQuery): string[] => {
return [...validateSortOrder(query)];
};
const validateSortOrder = (query: FindRulesRequestQuery): string[] => {
export const validateFindRulesRequestQuery = (query: FindRulesRequestQueryInput): string[] => {
if (query.sort_order != null || query.sort_field != null) {
if (query.sort_order == null || query.sort_field == null) {
return ['when "sort_order" and "sort_field" must exist together or not at all'];
} else {
return [];
}
} else {
return [];
}
return [];
};

View file

@ -6,6 +6,7 @@
*/
import { z } from 'zod';
import { BooleanFromString } from '@kbn/zod-helpers';
/*
* NOTICE: Do not edit this file manually.
@ -20,43 +21,19 @@ export const ImportRulesRequestQuery = z.object({
/**
* Determines whether existing rules with the same `rule_id` are overwritten.
*/
overwrite: z.preprocess(
(value: unknown) => (typeof value === 'boolean' ? String(value) : value),
z
.enum(['true', 'false'])
.default('false')
.transform((value) => value === 'true')
),
overwrite: BooleanFromString.optional().default(false),
/**
* Determines whether existing exception lists with the same `list_id` are overwritten.
*/
overwrite_exceptions: z.preprocess(
(value: unknown) => (typeof value === 'boolean' ? String(value) : value),
z
.enum(['true', 'false'])
.default('false')
.transform((value) => value === 'true')
),
overwrite_exceptions: BooleanFromString.optional().default(false),
/**
* Determines whether existing actions with the same `kibana.alert.rule.actions.id` are overwritten.
*/
overwrite_action_connectors: z.preprocess(
(value: unknown) => (typeof value === 'boolean' ? String(value) : value),
z
.enum(['true', 'false'])
.default('false')
.transform((value) => value === 'true')
),
overwrite_action_connectors: BooleanFromString.optional().default(false),
/**
* Generates a new list ID for each imported exception list.
*/
as_new_list: z.preprocess(
(value: unknown) => (typeof value === 'boolean' ? String(value) : value),
z
.enum(['true', 'false'])
.default('false')
.transform((value) => value === 'true')
),
as_new_list: BooleanFromString.optional().default(false),
});
export type ImportRulesRequestQueryInput = z.input<typeof ImportRulesRequestQuery>;

View file

@ -5,7 +5,8 @@
* 2.0.
*/
export * from './bulk_actions/bulk_actions_route';
export * from './bulk_actions/bulk_actions_types';
export * from './bulk_actions/bulk_actions_route.gen';
export * from './bulk_crud/bulk_create_rules/bulk_create_rules_route.gen';
export * from './bulk_crud/bulk_delete_rules/bulk_delete_rules_route.gen';
export * from './bulk_crud/bulk_patch_rules/bulk_patch_rules_route.gen';
@ -22,7 +23,7 @@ export * from './crud/update_rule/request_schema_validation';
export * from './crud/update_rule/update_rule_route.gen';
export * from './export_rules/export_rules_details_schema';
export * from './export_rules/export_rules_route.gen';
export * from './find_rules/find_rules_route';
export * from './find_rules/find_rules_route.gen';
export * from './find_rules/request_schema_validation';
export * from './get_rule_management_filters/get_rule_management_filters_route';
export * from './import_rules/import_rules_route.gen';

View file

@ -10,15 +10,15 @@ export * from './detection_engine_health/get_rule_health/get_rule_health_route';
export * from './detection_engine_health/get_space_health/get_space_health_route';
export * from './detection_engine_health/setup_health/setup_health_route';
export * from './detection_engine_health/model';
export * from './rule_execution_logs/get_rule_execution_events/get_rule_execution_events_route';
export * from './rule_execution_logs/get_rule_execution_events/get_rule_execution_events_route.gen';
export * from './rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.gen';
export * from './urls';
export * from './model/execution_event';
export * from './model/execution_metrics';
export * from './model/execution_event.gen';
export * from './model/execution_metrics.gen';
export * from './model/execution_result.gen';
export * from './model/execution_settings';
export * from './model/execution_status.gen';
export * from './model/execution_status';
export * from './model/execution_summary';
export * from './model/execution_summary.gen';
export * from './model/log_level';

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 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 LogLevel = z.infer<typeof LogLevel>;
export const LogLevel = z.enum(['trace', 'debug', 'info', 'warn', 'error']);
export type LogLevelEnum = typeof LogLevel.enum;
export const LogLevelEnum = LogLevel.enum;
/**
* Type of a plain rule execution event:
- message: Simple log message of some log level, such as debug, info or error.
- status-change: We log an event of this type each time a rule changes its status during an execution.
- execution-metrics: We log an event of this type at the end of a rule execution. It contains various execution metrics such as search and indexing durations.
*/
export type RuleExecutionEventType = z.infer<typeof RuleExecutionEventType>;
export const RuleExecutionEventType = z.enum(['message', 'status-change', 'execution-metrics']);
export type RuleExecutionEventTypeEnum = typeof RuleExecutionEventType.enum;
export const RuleExecutionEventTypeEnum = RuleExecutionEventType.enum;
/**
* Plain rule execution event. A rule can write many of them during each execution. Events can be of different types and log levels.
NOTE: This is a read model of rule execution events and it is pretty generic. It contains only a subset of their fields: only those fields that are common to all types of execution events.
*/
export type RuleExecutionEvent = z.infer<typeof RuleExecutionEvent>;
export const RuleExecutionEvent = z.object({
timestamp: z.string().datetime(),
sequence: z.number().int(),
level: LogLevel,
type: RuleExecutionEventType,
execution_id: z.string().min(1),
message: z.string(),
});

View file

@ -5,9 +5,8 @@
* 2.0.
*/
import type { RuleExecutionEvent } from './execution_event';
import { RuleExecutionEventType } from './execution_event';
import { LogLevel } from './log_level';
import type { RuleExecutionEvent } from './execution_event.gen';
import { LogLevelEnum, RuleExecutionEventTypeEnum } from './execution_event.gen';
const DEFAULT_TIMESTAMP = '2021-12-28T10:10:00.806Z';
const DEFAULT_SEQUENCE_NUMBER = 0;
@ -17,13 +16,13 @@ const getMessageEvent = (props: Partial<RuleExecutionEvent> = {}): RuleExecution
// Default values
timestamp: DEFAULT_TIMESTAMP,
sequence: DEFAULT_SEQUENCE_NUMBER,
level: LogLevel.debug,
level: LogLevelEnum.debug,
execution_id: 'execution-id-1',
message: 'Some message',
// Overridden values
...props,
// Mandatory values for this type of event
type: RuleExecutionEventType.message,
type: RuleExecutionEventTypeEnum.message,
};
};
@ -37,8 +36,8 @@ const getRunningStatusChange = (props: Partial<RuleExecutionEvent> = {}): RuleEx
// Overridden values
...props,
// Mandatory values for this type of event
level: LogLevel.info,
type: RuleExecutionEventType['status-change'],
level: LogLevelEnum.info,
type: RuleExecutionEventTypeEnum['status-change'],
};
};
@ -54,8 +53,8 @@ const getPartialFailureStatusChange = (
// Overridden values
...props,
// Mandatory values for this type of event
level: LogLevel.warn,
type: RuleExecutionEventType['status-change'],
level: LogLevelEnum.warn,
type: RuleExecutionEventTypeEnum['status-change'],
};
};
@ -69,8 +68,8 @@ const getFailedStatusChange = (props: Partial<RuleExecutionEvent> = {}): RuleExe
// Overridden values
...props,
// Mandatory values for this type of event
level: LogLevel.error,
type: RuleExecutionEventType['status-change'],
level: LogLevelEnum.error,
type: RuleExecutionEventTypeEnum['status-change'],
};
};
@ -84,8 +83,8 @@ const getSucceededStatusChange = (props: Partial<RuleExecutionEvent> = {}): Rule
// Overridden values
...props,
// Mandatory values for this type of event
level: LogLevel.info,
type: RuleExecutionEventType['status-change'],
level: LogLevelEnum.info,
type: RuleExecutionEventTypeEnum['status-change'],
};
};
@ -99,8 +98,8 @@ const getExecutionMetricsEvent = (props: Partial<RuleExecutionEvent> = {}): Rule
// Overridden values
...props,
// Mandatory values for this type of event
level: LogLevel.debug,
type: RuleExecutionEventType['execution-metrics'],
level: LogLevelEnum.debug,
type: RuleExecutionEventTypeEnum['execution-metrics'],
};
};
@ -120,7 +119,7 @@ const getSomeEvents = (): RuleExecutionEvent[] => [
getMessageEvent({
timestamp: '2021-12-28T10:10:06.806Z',
sequence: 6,
level: LogLevel.debug,
level: LogLevelEnum.debug,
message: 'Rule execution started',
}),
getFailedStatusChange({
@ -138,7 +137,7 @@ const getSomeEvents = (): RuleExecutionEvent[] => [
getMessageEvent({
timestamp: '2021-12-28T10:10:02.806Z',
sequence: 2,
level: LogLevel.error,
level: LogLevelEnum.error,
message: 'Some error',
}),
getRunningStatusChange({
@ -148,7 +147,7 @@ const getSomeEvents = (): RuleExecutionEvent[] => [
getMessageEvent({
timestamp: '2021-12-28T10:10:00.806Z',
sequence: 0,
level: LogLevel.debug,
level: LogLevelEnum.debug,
message: 'Rule execution started',
}),
];

View file

@ -0,0 +1,49 @@
openapi: '3.0.0'
info:
title: Execution Event Schema
version: 'not applicable'
paths: {}
components:
x-codegen-enabled: true
schemas:
LogLevel:
type: string
enum: ['trace', 'debug', 'info', 'warn', 'error']
RuleExecutionEventType:
type: string
enum: ['message', 'status-change', 'execution-metrics']
description: |-
Type of a plain rule execution event:
- message: Simple log message of some log level, such as debug, info or error.
- status-change: We log an event of this type each time a rule changes its status during an execution.
- execution-metrics: We log an event of this type at the end of a rule execution. It contains various execution metrics such as search and indexing durations.
RuleExecutionEvent:
type: object
properties:
timestamp:
type: string
format: date-time
sequence:
type: integer
level:
$ref: '#/components/schemas/LogLevel'
type:
$ref: '#/components/schemas/RuleExecutionEventType'
execution_id:
type: string
minLength: 1
message:
type: string
required:
- timestamp
- sequence
- level
- type
- execution_id
- message
description: |-
Plain rule execution event. A rule can write many of them during each execution. Events can be of different types and log levels.
NOTE: This is a read model of rule execution events and it is pretty generic. It contains only a subset of their fields: only those fields that are common to all types of execution events.

View file

@ -1,61 +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 { enumeration, IsoDateString, NonEmptyString } from '@kbn/securitysolution-io-ts-types';
import { enumFromString } from '../../../../utils/enum_from_string';
import { TLogLevel } from './log_level';
/**
* Type of a plain rule execution event.
*/
export enum RuleExecutionEventType {
/**
* Simple log message of some log level, such as debug, info or error.
*/
'message' = 'message',
/**
* We log an event of this type each time a rule changes its status during an execution.
*/
'status-change' = 'status-change',
/**
* We log an event of this type at the end of a rule execution. It contains various execution
* metrics such as search and indexing durations.
*/
'execution-metrics' = 'execution-metrics',
}
export const TRuleExecutionEventType = enumeration(
'RuleExecutionEventType',
RuleExecutionEventType
);
/**
* An array of supported types of rule execution events.
*/
export const RULE_EXECUTION_EVENT_TYPES = Object.values(RuleExecutionEventType);
export const ruleExecutionEventTypeFromString = enumFromString(RuleExecutionEventType);
/**
* Plain rule execution event. A rule can write many of them during each execution. Events can be
* of different types and log levels.
*
* NOTE: This is a read model of rule execution events and it is pretty generic. It contains only a
* subset of their fields: only those fields that are common to all types of execution events.
*/
export type RuleExecutionEvent = t.TypeOf<typeof RuleExecutionEvent>;
export const RuleExecutionEvent = t.type({
timestamp: IsoDateString,
sequence: t.number,
level: TLogLevel,
type: TRuleExecutionEventType,
execution_id: NonEmptyString,
message: t.string,
});

View file

@ -15,16 +15,19 @@ import { z } from 'zod';
export type RuleExecutionMetrics = z.infer<typeof RuleExecutionMetrics>;
export const RuleExecutionMetrics = z.object({
/**
* Total time spent searching for events
* Total time spent performing ES searches as measured by Kibana; includes network latency and time spent serializing/deserializing request/response
*/
total_search_duration_ms: z.number().int().min(0).optional(),
/**
* Total time spent indexing alerts
* Total time spent indexing documents during current rule execution cycle
*/
total_indexing_duration_ms: z.number().int().min(0).optional(),
/**
* Total time spent enriching documents during current rule execution cycle
*/
total_enrichment_duration_ms: z.number().int().min(0).optional(),
/**
* Time gap between last execution and current execution
* Duration in seconds of execution gap
*/
execution_gap_duration_s: z.number().int().min(0).optional(),
});

View file

@ -10,17 +10,18 @@ components:
type: object
properties:
total_search_duration_ms:
description: Total time spent searching for events
description: Total time spent performing ES searches as measured by Kibana; includes network latency and time spent serializing/deserializing request/response
type: integer
minimum: 0
total_indexing_duration_ms:
description: Total time spent indexing alerts
description: Total time spent indexing documents during current rule execution cycle
type: integer
minimum: 0
total_enrichment_duration_ms:
description: Total time spent enriching documents during current rule execution cycle
type: integer
minimum: 0
execution_gap_duration_s:
description: Time gap between last execution and current execution
description: Duration in seconds of execution gap
type: integer
minimum: 0

View file

@ -1,28 +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 { PositiveInteger } from '@kbn/securitysolution-io-ts-types';
export type DurationMetric = t.TypeOf<typeof DurationMetric>;
export const DurationMetric = PositiveInteger;
export type RuleExecutionMetrics = t.TypeOf<typeof RuleExecutionMetrics>;
/**
@property total_search_duration_ms - "total time spent performing ES searches as measured by Kibana;
includes network latency and time spent serializing/deserializing request/response",
@property total_indexing_duration_ms - "total time spent indexing documents during current rule execution cycle",
@property total_enrichment_duration_ms - total time spent enriching documents during current rule execution cycle
@property execution_gap_duration_s - "duration in seconds of execution gap"
*/
export const RuleExecutionMetrics = t.partial({
total_search_duration_ms: DurationMetric,
total_indexing_duration_ms: DurationMetric,
total_enrichment_duration_ms: DurationMetric,
execution_gap_duration_s: DurationMetric,
});

View file

@ -6,14 +6,10 @@
*/
import type { RuleLastRunOutcomes } from '@kbn/alerting-plugin/common';
import { enumeration } from '@kbn/securitysolution-io-ts-types';
import { assertUnreachable } from '../../../../utility_types';
import type { RuleExecutionStatus, RuleExecutionStatusOrder } from './execution_status.gen';
import { RuleExecutionStatusEnum } from './execution_status.gen';
// TODO remove after the migration to Zod is done
export const TRuleExecutionStatus = enumeration('RuleExecutionStatus', RuleExecutionStatusEnum);
export const ruleExecutionStatusToNumber = (
status: RuleExecutionStatus
): RuleExecutionStatusOrder => {

View file

@ -6,7 +6,7 @@
*/
import { RuleExecutionStatusEnum } from './execution_status.gen';
import type { RuleExecutionSummary } from './execution_summary';
import type { RuleExecutionSummary } from './execution_summary.gen';
const getSummarySucceeded = (): RuleExecutionSummary => ({
last_execution: {

View file

@ -1,22 +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 { IsoDateString } from '@kbn/securitysolution-io-ts-types';
import * as t from 'io-ts';
import { RuleExecutionMetrics } from './execution_metrics';
import { TRuleExecutionStatus } from './execution_status';
export type RuleExecutionSummary = t.TypeOf<typeof RuleExecutionSummary>;
export const RuleExecutionSummary = t.type({
last_execution: t.type({
date: IsoDateString,
status: TRuleExecutionStatus,
status_order: t.number,
message: t.string,
metrics: RuleExecutionMetrics,
}),
});

View file

@ -5,10 +5,10 @@
* 2.0.
*/
export * from './execution_event';
export * from './execution_metrics';
export * from './execution_event.gen';
export * from './execution_metrics.gen';
export * from './execution_result.gen';
export * from './execution_settings';
export * from './execution_status.gen';
export * from './execution_summary';
export * from './execution_summary.gen';
export * from './log_level';

View file

@ -5,42 +5,31 @@
* 2.0.
*/
import { enumeration } from '@kbn/securitysolution-io-ts-types';
import { enumFromString } from '../../../../utils/enum_from_string';
import { assertUnreachable } from '../../../../utility_types';
import type { RuleExecutionStatus } from './execution_status.gen';
import { RuleExecutionStatusEnum } from './execution_status.gen';
export enum LogLevel {
'trace' = 'trace',
'debug' = 'debug',
'info' = 'info',
'warn' = 'warn',
'error' = 'error',
}
export const TLogLevel = enumeration('LogLevel', LogLevel);
import { LogLevel, LogLevelEnum } from './execution_event.gen';
/**
* An array of supported log levels.
*/
export const LOG_LEVELS = Object.values(LogLevel);
export const LOG_LEVELS = LogLevel.options;
export const logLevelToNumber = (level: keyof typeof LogLevel | null | undefined): number => {
export const logLevelToNumber = (level: LogLevel | null | undefined): number => {
if (!level) {
return 0;
}
switch (level) {
case 'trace':
case LogLevelEnum.trace:
return 0;
case 'debug':
case LogLevelEnum.debug:
return 10;
case 'info':
case LogLevelEnum.info:
return 20;
case 'warn':
case LogLevelEnum.warn:
return 30;
case 'error':
case LogLevelEnum.error:
return 40;
default:
assertUnreachable(level);
@ -50,34 +39,32 @@ export const logLevelToNumber = (level: keyof typeof LogLevel | null | undefined
export const logLevelFromNumber = (num: number | null | undefined): LogLevel => {
if (num === null || num === undefined || num < 10) {
return LogLevel.trace;
return LogLevelEnum.trace;
}
if (num < 20) {
return LogLevel.debug;
return LogLevelEnum.debug;
}
if (num < 30) {
return LogLevel.info;
return LogLevelEnum.info;
}
if (num < 40) {
return LogLevel.warn;
return LogLevelEnum.warn;
}
return LogLevel.error;
return LogLevelEnum.error;
};
export const logLevelFromString = enumFromString(LogLevel);
export const logLevelFromExecutionStatus = (status: RuleExecutionStatus): LogLevel => {
switch (status) {
case RuleExecutionStatusEnum['going to run']:
case RuleExecutionStatusEnum.running:
case RuleExecutionStatusEnum.succeeded:
return LogLevel.info;
return LogLevelEnum.info;
case RuleExecutionStatusEnum['partial failure']:
return LogLevel.warn;
return LogLevelEnum.warn;
case RuleExecutionStatusEnum.failed:
return LogLevel.error;
return LogLevelEnum.error;
default:
assertUnreachable(status);
return LogLevel.trace;
return LogLevelEnum.trace;
}
};

View file

@ -6,48 +6,43 @@
*/
import { z } from 'zod';
import { ArrayFromString } from '@kbn/zod-helpers';
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*/
import { RuleExecutionStatus } from '../../model/execution_status.gen';
import {
SortFieldOfRuleExecutionResult,
RuleExecutionResult,
} from '../../model/execution_result.gen';
RuleExecutionEventType,
LogLevel,
RuleExecutionEvent,
} from '../../model/execution_event.gen';
import { SortOrder } from '../../../model/sorting.gen';
import { PaginationResult } from '../../../model/pagination.gen';
export type GetRuleExecutionEventsRequestQuery = z.infer<typeof GetRuleExecutionEventsRequestQuery>;
export const GetRuleExecutionEventsRequestQuery = z.object({
/**
* Include events of matching the search term. If omitted, all events will be included.
*/
search_term: z.string().optional(),
/**
* Include events of the specified types. If omitted, all types of events will be included.
*/
event_types: ArrayFromString(RuleExecutionEventType).optional().default([]),
/**
* Include events having these log levels. If omitted, events of all levels will be included.
*/
log_levels: ArrayFromString(LogLevel).optional().default([]),
/**
* Start date of the time range to query
*/
start: z.string().datetime(),
date_start: z.string().datetime().optional(),
/**
* End date of the time range to query
*/
end: z.string().datetime(),
/**
* Query text to filter results by
*/
query_text: z.string().optional().default(''),
/**
* Comma-separated list of rule execution statuses to filter results by
*/
status_filters: z
.preprocess(
(value: unknown) =>
typeof value === 'string' ? (value === '' ? [] : value.split(',')) : value,
z.array(RuleExecutionStatus)
)
.optional()
.default([]),
/**
* Field to sort results by
*/
sort_field: SortFieldOfRuleExecutionResult.optional().default('timestamp'),
date_end: z.string().datetime().optional(),
/**
* Sort order to sort results by
*/
@ -69,9 +64,6 @@ export type GetRuleExecutionEventsRequestParams = z.infer<
typeof GetRuleExecutionEventsRequestParams
>;
export const GetRuleExecutionEventsRequestParams = z.object({
/**
* Saved object ID of the rule to get execution results for
*/
ruleId: z.string().min(1),
});
export type GetRuleExecutionEventsRequestParamsInput = z.input<
@ -80,6 +72,6 @@ export type GetRuleExecutionEventsRequestParamsInput = z.input<
export type GetRuleExecutionEventsResponse = z.infer<typeof GetRuleExecutionEventsResponse>;
export const GetRuleExecutionEventsResponse = z.object({
events: z.array(RuleExecutionResult).optional(),
total: z.number().int().optional(),
events: z.array(RuleExecutionEvent),
pagination: PaginationResult,
});

View file

@ -6,7 +6,7 @@
*/
import { ruleExecutionEventMock } from '../../model/execution_event.mock';
import type { GetRuleExecutionEventsResponse } from './get_rule_execution_events_route';
import type { GetRuleExecutionEventsResponse } from './get_rule_execution_events_route.gen';
const getSomeResponse = (): GetRuleExecutionEventsResponse => {
const events = ruleExecutionEventMock.getSomeEvents();

View file

@ -14,47 +14,47 @@ paths:
- name: ruleId
in: path
required: true
description: Saved object ID of the rule to get execution results for
schema:
type: string
minLength: 1
- name: start
- name: search_term
in: query
required: true
required: false
description: Include events of matching the search term. If omitted, all events will be included.
schema:
type: string
- name: event_types
in: query
required: false
description: Include events of the specified types. If omitted, all types of events will be included.
schema:
type: array
items:
$ref: '../../model/execution_event.schema.yaml#/components/schemas/RuleExecutionEventType'
default: []
- name: log_levels
in: query
required: false
description: Include events having these log levels. If omitted, events of all levels will be included.
schema:
type: array
items:
$ref: '../../model/execution_event.schema.yaml#/components/schemas/LogLevel'
default: []
- name: date_start
in: query
required: false
description: Start date of the time range to query
schema:
type: string
format: date-time
- name: end
- name: date_end
in: query
required: true
required: false
description: End date of the time range to query
schema:
type: string
format: date-time
- name: query_text
in: query
required: false
description: Query text to filter results by
schema:
type: string
default: ''
- name: status_filters
in: query
required: false
description: Comma-separated list of rule execution statuses to filter results by
schema:
type: array
items:
$ref: '../../model/execution_status.schema.yaml#/components/schemas/RuleExecutionStatus'
default: []
- name: sort_field
in: query
required: false
description: Field to sort results by
schema:
$ref: '../../model/execution_result.schema.yaml#/components/schemas/SortFieldOfRuleExecutionResult'
default: timestamp
- name: sort_order
in: query
required: false
@ -87,6 +87,9 @@ paths:
events:
type: array
items:
$ref: '../../model/execution_result.schema.yaml#/components/schemas/RuleExecutionResult'
total:
type: integer
$ref: '../../model/execution_event.schema.yaml#/components/schemas/RuleExecutionEvent'
pagination:
$ref: '../../../model/pagination.schema.yaml#/components/schemas/PaginationResult'
required:
- events
- pagination

View file

@ -5,14 +5,11 @@
* 2.0.
*/
import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
import { expectParseError, expectParseSuccess } from '@kbn/zod-helpers';
import {
GetRuleExecutionEventsRequestParams,
GetRuleExecutionEventsRequestQuery,
} from './get_rule_execution_events_route';
} from './get_rule_execution_events_route.gen';
describe('Request schema of Get rule execution events', () => {
describe('GetRuleExecutionEventsRequestParams', () => {
@ -22,11 +19,10 @@ describe('Request schema of Get rule execution events', () => {
ruleId: 'some id',
};
const decoded = GetRuleExecutionEventsRequestParams.decode(input);
const message = pipe(decoded, foldLeftRight);
const results = GetRuleExecutionEventsRequestParams.safeParse(input);
expectParseSuccess(results);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(
expect(results.data).toEqual(
expect.objectContaining({
ruleId: 'some id',
})
@ -39,23 +35,21 @@ describe('Request schema of Get rule execution events', () => {
foo: 'bar', // this one is not in the schema and will be stripped
};
const decoded = GetRuleExecutionEventsRequestParams.decode(input);
const message = pipe(decoded, foldLeftRight);
const results = GetRuleExecutionEventsRequestParams.safeParse(input);
expectParseSuccess(results);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({
ruleId: 'some id',
});
expect(results.data).toEqual(
expect.objectContaining({
ruleId: 'some id',
})
);
});
});
describe('Validation fails', () => {
const test = (input: unknown) => {
const decoded = GetRuleExecutionEventsRequestParams.decode(input);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors)).length).toBeGreaterThan(0);
expect(message.schema).toEqual({});
const results = GetRuleExecutionEventsRequestParams.safeParse(input);
expectParseError(results);
};
it('when not all the required parameters are passed', () => {
@ -84,11 +78,10 @@ describe('Request schema of Get rule execution events', () => {
per_page: 6,
};
const decoded = GetRuleExecutionEventsRequestQuery.decode(input);
const message = pipe(decoded, foldLeftRight);
const result = GetRuleExecutionEventsRequestQuery.safeParse(input);
expectParseSuccess(result);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({
expect(result.data).toEqual({
event_types: ['message', 'status-change'],
log_levels: ['debug', 'info', 'error'],
sort_order: 'asc',
@ -107,11 +100,10 @@ describe('Request schema of Get rule execution events', () => {
foo: 'bar', // this one is not in the schema and will be stripped
};
const decoded = GetRuleExecutionEventsRequestQuery.decode(input);
const message = pipe(decoded, foldLeftRight);
const result = GetRuleExecutionEventsRequestQuery.safeParse(input);
expectParseSuccess(result);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({
expect(result.data).toEqual({
event_types: ['message', 'status-change'],
log_levels: ['debug', 'info', 'error'],
sort_order: 'asc',
@ -119,25 +111,12 @@ describe('Request schema of Get rule execution events', () => {
per_page: 6,
});
});
it('when no parameters are passed (all are have default values)', () => {
const input = {};
const decoded = GetRuleExecutionEventsRequestQuery.decode(input);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(expect.any(Object));
});
});
describe('Validation fails', () => {
const test = (input: unknown) => {
const decoded = GetRuleExecutionEventsRequestQuery.decode(input);
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors)).length).toBeGreaterThan(0);
expect(message.schema).toEqual({});
const result = GetRuleExecutionEventsRequestQuery.safeParse(input);
expectParseError(result);
};
it('when invalid parameters are passed', () => {
@ -147,21 +126,18 @@ describe('Request schema of Get rule execution events', () => {
});
});
describe('Validation sets default values', () => {
it('when optional parameters are not passed', () => {
const input = {};
it('Validation sets default values when optional parameters are not passed', () => {
const input = {};
const decoded = GetRuleExecutionEventsRequestQuery.decode(input);
const message = pipe(decoded, foldLeftRight);
const result = GetRuleExecutionEventsRequestQuery.safeParse(input);
expectParseSuccess(result);
expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({
event_types: [],
log_levels: [],
sort_order: 'desc',
page: 1,
per_page: 20,
});
expect(result.data).toEqual({
event_types: [],
log_levels: [],
sort_order: 'desc',
page: 1,
per_page: 20,
});
});
});

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 { DefaultPerPage, DefaultPage } from '@kbn/securitysolution-io-ts-alerting-types';
import { defaultCsvArray, IsoDateString, NonEmptyString } from '@kbn/securitysolution-io-ts-types';
import { DefaultSortOrderDesc, PaginationResult } from '../../../model';
import { RuleExecutionEvent, TRuleExecutionEventType, TLogLevel } from '../../model';
/**
* URL path parameters of the API route.
*/
export type GetRuleExecutionEventsRequestParams = t.TypeOf<
typeof GetRuleExecutionEventsRequestParams
>;
export const GetRuleExecutionEventsRequestParams = t.exact(
t.type({
ruleId: NonEmptyString,
})
);
/**
* Query string parameters of the API route.
*/
export type GetRuleExecutionEventsRequestQuery = t.TypeOf<
typeof GetRuleExecutionEventsRequestQuery
>;
export const GetRuleExecutionEventsRequestQuery = t.exact(
t.intersection([
t.partial({
search_term: NonEmptyString,
event_types: defaultCsvArray(TRuleExecutionEventType),
log_levels: defaultCsvArray(TLogLevel),
date_start: IsoDateString,
date_end: IsoDateString,
}),
t.type({
sort_order: DefaultSortOrderDesc, // defaults to 'desc'
page: DefaultPage, // defaults to 1
per_page: DefaultPerPage, // defaults to 20
}),
])
);
/**
* Response body of the API route.
*/
export type GetRuleExecutionEventsResponse = t.TypeOf<typeof GetRuleExecutionEventsResponse>;
export const GetRuleExecutionEventsResponse = t.exact(
t.type({
events: t.array(RuleExecutionEvent),
pagination: PaginationResult,
})
);

View file

@ -6,6 +6,7 @@
*/
import { z } from 'zod';
import { ArrayFromString } from '@kbn/zod-helpers';
/*
* NOTICE: Do not edit this file manually.
@ -38,14 +39,7 @@ export const GetRuleExecutionResultsRequestQuery = z.object({
/**
* Comma-separated list of rule execution statuses to filter results by
*/
status_filters: z
.preprocess(
(value: unknown) =>
typeof value === 'string' ? (value === '' ? [] : value.split(',')) : value,
z.array(RuleExecutionStatus)
)
.optional()
.default([]),
status_filters: ArrayFromString(RuleExecutionStatus).optional().default([]),
/**
* Field to sort results by
*/

View file

@ -8,9 +8,6 @@
import * as t from 'io-ts';
import { PositiveInteger, PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
import { IndexPatternArray } from '../../model/rule_schema_legacy';
export const signalsReindexOptions = t.partial({
requests_per_second: t.number,
@ -23,7 +20,7 @@ export type SignalsReindexOptions = t.TypeOf<typeof signalsReindexOptions>;
export const createSignalsMigrationSchema = t.intersection([
t.exact(
t.type({
index: IndexPatternArray,
index: t.array(t.string),
})
),
t.exact(signalsReindexOptions),

View file

@ -21,7 +21,7 @@ import {
SavedObjectResolveAliasTargetId,
SavedObjectResolveOutcome,
} from '../../detection_engine/model/rule_schema_legacy';
import { ErrorSchema, success, success_count as successCount } from '../../detection_engine';
import { ErrorSchema } from './error_schema';
export const BareNoteSchema = runtimeTypes.intersection([
runtimeTypes.type({
@ -497,8 +497,8 @@ export interface ExportTimelineNotFoundError {
export const importTimelineResultSchema = runtimeTypes.exact(
runtimeTypes.type({
success,
success_count: successCount,
success: runtimeTypes.boolean,
success_count: PositiveInteger,
timelines_installed: PositiveInteger,
timelines_updated: PositiveInteger,
errors: runtimeTypes.array(ErrorSchema),

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ErrorSchema } from './error_schema';
export const getErrorSchemaMock = (
id: string = '819eded6-e9c8-445b-a647-519aea39e063'
): ErrorSchema => ({
id,
error: {
status_code: 404,
message: 'id: "819eded6-e9c8-445b-a647-519aea39e063" not found',
},
});

View file

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

View file

@ -5,22 +5,16 @@
* 2.0.
*/
import { NonEmptyString } from '@kbn/securitysolution-io-ts-types';
import { NonEmptyString, PositiveInteger } from '@kbn/securitysolution-io-ts-types';
import * as t from 'io-ts';
// 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
// sometimes echo back out the id that the user gave us and it is not guaranteed
// to be a UUID but rather just a string
const partial = t.exact(
t.partial({
id: t.string,
rule_id: RuleSignatureId,
rule_id: NonEmptyString,
list_id: NonEmptyString,
item_id: NonEmptyString,
})
@ -28,8 +22,8 @@ const partial = t.exact(
const required = t.exact(
t.type({
error: t.type({
status_code,
message,
status_code: PositiveInteger,
message: t.string,
}),
})
);

View file

@ -17,8 +17,8 @@ import type {
ResponseAction,
RuleResponseAction,
} from '../api/detection_engine/model/rule_response_actions';
import { RESPONSE_ACTION_TYPES } from '../api/detection_engine/model/rule_response_actions';
import type { NormalizedRuleAction } from '../api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { ResponseActionTypesEnum } from '../api/detection_engine/model/rule_response_actions';
import type { NormalizedRuleAction } from '../api/detection_engine/rule_management';
import type { RuleAction } from '@kbn/alerting-plugin/common';
describe('transform_actions', () => {
@ -93,7 +93,7 @@ describe('transform_actions', () => {
});
test('it should transform ResponseAction[] to RuleResponseAction[]', () => {
const ruleAction: ResponseAction = {
action_type_id: RESPONSE_ACTION_TYPES.OSQUERY,
action_type_id: ResponseActionTypesEnum['.osquery'],
params: {
ecs_mapping: {},
saved_query_id: undefined,
@ -117,7 +117,7 @@ describe('transform_actions', () => {
test('it should transform RuleResponseAction[] to ResponseAction[]', () => {
const alertAction: RuleResponseAction = {
actionTypeId: RESPONSE_ACTION_TYPES.OSQUERY,
actionTypeId: ResponseActionTypesEnum['.osquery'],
params: {
ecsMapping: {},
savedQueryId: undefined,

View file

@ -7,12 +7,12 @@
import type { RuleAction as AlertingRuleAction } from '@kbn/alerting-plugin/common';
import type { NormalizedAlertAction } from '@kbn/alerting-plugin/server/rules_client';
import type { NormalizedRuleAction } from '../api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import type { NormalizedRuleAction } from '../api/detection_engine/rule_management';
import type {
ResponseAction,
RuleResponseAction,
} from '../api/detection_engine/model/rule_response_actions';
import { RESPONSE_ACTION_TYPES } from '../api/detection_engine/model/rule_response_actions';
import { ResponseActionTypesEnum } from '../api/detection_engine/model/rule_response_actions';
import type { RuleAction } from '../api/detection_engine/model';
export const transformRuleToAlertAction = ({
@ -63,7 +63,12 @@ export const transformNormalizedRuleToAlertAction = ({
group,
id,
params: params as AlertingRuleAction['params'],
...(alertsFilter && { alertsFilter }),
...(alertsFilter && {
// We use "unknown" as the alerts filter type which is stricter than the one
// used in the alerting plugin (what they use is essentially "any"). So we
// have to to cast here
alertsFilter: alertsFilter as AlertingRuleAction['alertsFilter'],
}),
...(frequency && { frequency }),
});
@ -85,7 +90,7 @@ export const transformRuleToAlertResponseAction = ({
action_type_id: actionTypeId,
params,
}: ResponseAction): RuleResponseAction => {
if (actionTypeId === RESPONSE_ACTION_TYPES.OSQUERY) {
if (actionTypeId === ResponseActionTypesEnum['.osquery']) {
const {
saved_query_id: savedQueryId,
ecs_mapping: ecsMapping,
@ -113,7 +118,7 @@ export const transformAlertToRuleResponseAction = ({
actionTypeId,
params,
}: RuleResponseAction): ResponseAction => {
if (actionTypeId === RESPONSE_ACTION_TYPES.OSQUERY) {
if (actionTypeId === ResponseActionTypesEnum['.osquery']) {
const { savedQueryId, ecsMapping, packId, ...rest } = params;
return {
params: {

View file

@ -6,9 +6,6 @@
*/
import * as t from 'io-ts';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
import { DataViewId } from '../../api/detection_engine/model/rule_schema_legacy';
import { afterKeysSchema } from '../after_keys';
import { identifierTypeSchema } from '../identifier_types';
import { riskWeightsSchema } from '../risk_weights/schema';
@ -16,7 +13,7 @@ import { riskWeightsSchema } from '../risk_weights/schema';
export const riskScoreCalculationRequestSchema = t.exact(
t.intersection([
t.type({
data_view_id: DataViewId,
data_view_id: t.string,
identifier_type: identifierTypeSchema,
range: t.type({
start: t.string,

View file

@ -6,9 +6,6 @@
*/
import * as t from 'io-ts';
// TODO https://github.com/elastic/security-team/issues/7491
// eslint-disable-next-line no-restricted-imports
import { DataViewId } from '../../api/detection_engine/model/rule_schema_legacy';
import { afterKeysSchema } from '../after_keys';
import { identifierTypeSchema } from '../identifier_types';
import { rangeSchema } from '../range';
@ -17,7 +14,7 @@ import { riskWeightsSchema } from '../risk_weights/schema';
export const riskScorePreviewRequestSchema = t.exact(
t.intersection([
t.type({
data_view_id: DataViewId,
data_view_id: t.string,
}),
t.partial({
after_keys: afterKeysSchema,

View file

@ -13,7 +13,7 @@ export interface RawEventData {
_index: string;
}
export enum RESPONSE_ACTION_TYPES {
export enum ResponseActionTypesEnum {
OSQUERY = '.osquery',
ENDPOINT = '.endpoint',
}
@ -34,7 +34,7 @@ export interface ExpandedEventFieldsObject {
type RuleParameters = Array<{
response_actions: Array<{
action_type_id: RESPONSE_ACTION_TYPES;
action_type_id: ResponseActionTypesEnum;
params: Record<string, unknown>;
}>;
}>;

View file

@ -20,7 +20,7 @@ import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_fe
import { useKibana } from '../../lib/kibana';
import { EventsViewType } from './event_details';
import * as i18n from './translations';
import { RESPONSE_ACTION_TYPES } from '../../../../common/api/detection_engine/model/rule_response_actions';
import { ResponseActionTypesEnum } from '../../../../common/api/detection_engine/model/rule_response_actions';
const TabContentWrapper = styled.div`
height: 100%;
@ -71,7 +71,7 @@ export const useOsqueryTab = ({
}
const osqueryResponseActions = responseActions.filter(
(responseAction) => responseAction.action_type_id === RESPONSE_ACTION_TYPES.OSQUERY
(responseAction) => responseAction.action_type_id === ResponseActionTypesEnum['.osquery']
);
if (!osqueryResponseActions?.length) {

View file

@ -16,9 +16,9 @@ import {
getRulesSchemaMock,
} from '../../../../common/api/detection_engine/model/rule_schema/mocks';
import {
BulkActionType,
BulkActionEditType,
} from '../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
BulkActionTypeEnum,
BulkActionEditTypeEnum,
} from '../../../../common/api/detection_engine/rule_management';
import { rulesMock } from '../logic/mock';
import type { FindRulesReferencedByExceptionsListProp } from '../logic/types';
@ -701,7 +701,9 @@ describe('Detections Rules API', () => {
});
test('passes a query', async () => {
await performBulkAction({ bulkAction: { type: BulkActionType.enable, query: 'some query' } });
await performBulkAction({
bulkAction: { type: BulkActionTypeEnum.enable, query: 'some query' },
});
expect(fetchMock).toHaveBeenCalledWith(
'/api/detection_engine/rules/_bulk_action',
@ -720,7 +722,7 @@ describe('Detections Rules API', () => {
test('passes ids', async () => {
await performBulkAction({
bulkAction: { type: BulkActionType.disable, ids: ['ruleId1', 'ruleId2'] },
bulkAction: { type: BulkActionTypeEnum.disable, ids: ['ruleId1', 'ruleId2'] },
});
expect(fetchMock).toHaveBeenCalledWith(
@ -741,10 +743,10 @@ describe('Detections Rules API', () => {
test('passes edit payload', async () => {
await performBulkAction({
bulkAction: {
type: BulkActionType.edit,
type: BulkActionTypeEnum.edit,
ids: ['ruleId1'],
editPayload: [
{ type: BulkActionEditType.add_index_patterns, value: ['some-index-pattern'] },
{ type: BulkActionEditTypeEnum.add_index_patterns, value: ['some-index-pattern'] },
],
},
});
@ -767,7 +769,7 @@ describe('Detections Rules API', () => {
test('executes dry run', async () => {
await performBulkAction({
bulkAction: { type: BulkActionType.disable, query: 'some query' },
bulkAction: { type: BulkActionTypeEnum.disable, query: 'some query' },
dryRun: true,
});
@ -787,7 +789,7 @@ describe('Detections Rules API', () => {
test('returns result', async () => {
const result = await performBulkAction({
bulkAction: {
type: BulkActionType.disable,
type: BulkActionTypeEnum.disable,
query: 'some query',
},
});

View file

@ -27,12 +27,16 @@ import type {
ReviewRuleInstallationResponseBody,
} from '../../../../common/api/detection_engine/prebuilt_rules';
import type {
BulkDuplicateRules,
BulkActionEditPayload,
BulkActionType,
CoverageOverviewResponse,
GetRuleManagementFiltersResponse,
} from '../../../../common/api/detection_engine/rule_management';
import {
RULE_MANAGEMENT_FILTERS_URL,
RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL,
BulkActionTypeEnum,
} from '../../../../common/api/detection_engine/rule_management';
import type { BulkActionsDryRunErrCode } from '../../../../common/constants';
import {
@ -54,11 +58,6 @@ import {
import type { RulesReferencedByExceptionListsSchema } from '../../../../common/api/detection_engine/rule_exceptions';
import { DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL } from '../../../../common/api/detection_engine/rule_exceptions';
import type {
BulkActionDuplicatePayload,
BulkActionEditPayload,
} from '../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionType } from '../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import type { PreviewResponse, RuleResponse } from '../../../../common/api/detection_engine';
import { KibanaServices } from '../../../common/lib/kibana';
@ -331,18 +330,18 @@ export type QueryOrIds = { query: string; ids?: undefined } | { query?: undefine
type PlainBulkAction = {
type: Exclude<
BulkActionType,
BulkActionType.edit | BulkActionType.export | BulkActionType.duplicate
BulkActionTypeEnum['edit'] | BulkActionTypeEnum['export'] | BulkActionTypeEnum['duplicate']
>;
} & QueryOrIds;
type EditBulkAction = {
type: BulkActionType.edit;
type: BulkActionTypeEnum['edit'];
editPayload: BulkActionEditPayload[];
} & QueryOrIds;
type DuplicateBulkAction = {
type: BulkActionType.duplicate;
duplicatePayload?: BulkActionDuplicatePayload;
type: BulkActionTypeEnum['duplicate'];
duplicatePayload?: BulkDuplicateRules['duplicate'];
} & QueryOrIds;
export type BulkAction = PlainBulkAction | EditBulkAction | DuplicateBulkAction;
@ -368,9 +367,9 @@ export async function performBulkAction({
action: bulkAction.type,
query: bulkAction.query,
ids: bulkAction.ids,
edit: bulkAction.type === BulkActionType.edit ? bulkAction.editPayload : undefined,
edit: bulkAction.type === BulkActionTypeEnum.edit ? bulkAction.editPayload : undefined,
duplicate:
bulkAction.type === BulkActionType.duplicate ? bulkAction.duplicatePayload : undefined,
bulkAction.type === BulkActionTypeEnum.duplicate ? bulkAction.duplicatePayload : undefined,
};
return KibanaServices.get().http.fetch<BulkActionResponse>(DETECTION_ENGINE_RULES_BULK_ACTION, {
@ -392,7 +391,7 @@ export type BulkExportResponse = Blob;
*/
export async function bulkExportRules(queryOrIds: QueryOrIds): Promise<BulkExportResponse> {
const params = {
action: BulkActionType.export,
action: BulkActionTypeEnum.export,
query: queryOrIds.query,
ids: queryOrIds.ids,
};

View file

@ -7,7 +7,7 @@
import type { UseMutationOptions } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import type { IHttpFetchError } from '@kbn/core/public';
import { BulkActionType } from '../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionTypeEnum } from '../../../../../common/api/detection_engine/rule_management';
import type { BulkActionErrorResponse, BulkActionResponse, PerformBulkActionProps } from '../api';
import { performBulkAction } from '../api';
import { DETECTION_ENGINE_RULES_BULK_ACTION } from '../../../../../common/constants';
@ -59,8 +59,8 @@ export const useBulkActionMutation = (
response?.attributes?.results?.updated ?? error?.body?.attributes?.results?.updated;
switch (actionType) {
case BulkActionType.enable:
case BulkActionType.disable: {
case BulkActionTypeEnum.enable:
case BulkActionTypeEnum.disable: {
invalidateFetchRuleByIdQuery();
invalidateFetchCoverageOverviewQuery();
if (updatedRules) {
@ -72,7 +72,7 @@ export const useBulkActionMutation = (
}
break;
}
case BulkActionType.delete:
case BulkActionTypeEnum.delete:
invalidateFindRulesQuery();
invalidateFetchRuleByIdQuery();
invalidateFetchRuleManagementFilters();
@ -81,12 +81,12 @@ export const useBulkActionMutation = (
invalidateFetchPrebuiltRulesUpgradeReviewQuery();
invalidateFetchCoverageOverviewQuery();
break;
case BulkActionType.duplicate:
case BulkActionTypeEnum.duplicate:
invalidateFindRulesQuery();
invalidateFetchRuleManagementFilters();
invalidateFetchCoverageOverviewQuery();
break;
case BulkActionType.edit:
case BulkActionTypeEnum.edit:
if (updatedRules) {
// We have a list of updated rules, no need to invalidate all
updateRulesCache(updatedRules);

View file

@ -6,54 +6,57 @@
*/
import type { HTTPError } from '../../../../../common/detection_engine/types';
import type { BulkActionEditPayload } from '../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import {
BulkActionEditType,
import type {
BulkActionEditPayload,
BulkActionType,
} from '../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
} from '../../../../../common/api/detection_engine/rule_management';
import {
BulkActionEditTypeEnum,
BulkActionTypeEnum,
} from '../../../../../common/api/detection_engine/rule_management';
import * as i18n from '../../../../detections/pages/detection_engine/rules/translations';
import type { BulkActionResponse, BulkActionSummary } from '../../api/api';
export function summarizeBulkSuccess(action: BulkActionType): string {
switch (action) {
case BulkActionType.export:
case BulkActionTypeEnum.export:
return i18n.RULES_BULK_EXPORT_SUCCESS;
case BulkActionType.duplicate:
case BulkActionTypeEnum.duplicate:
return i18n.RULES_BULK_DUPLICATE_SUCCESS;
case BulkActionType.delete:
case BulkActionTypeEnum.delete:
return i18n.RULES_BULK_DELETE_SUCCESS;
case BulkActionType.enable:
case BulkActionTypeEnum.enable:
return i18n.RULES_BULK_ENABLE_SUCCESS;
case BulkActionType.disable:
case BulkActionTypeEnum.disable:
return i18n.RULES_BULK_DISABLE_SUCCESS;
case BulkActionType.edit:
case BulkActionTypeEnum.edit:
return i18n.RULES_BULK_EDIT_SUCCESS;
}
}
export function explainBulkSuccess(
action: Exclude<BulkActionType, BulkActionType.edit>,
action: Exclude<BulkActionType, BulkActionTypeEnum['edit']>,
summary: BulkActionSummary
): string {
switch (action) {
case BulkActionType.export:
case BulkActionTypeEnum.export:
return getExportSuccessToastMessage(summary.succeeded, summary.total);
case BulkActionType.duplicate:
case BulkActionTypeEnum.duplicate:
return i18n.RULES_BULK_DUPLICATE_SUCCESS_DESCRIPTION(summary.succeeded);
case BulkActionType.delete:
case BulkActionTypeEnum.delete:
return i18n.RULES_BULK_DELETE_SUCCESS_DESCRIPTION(summary.succeeded);
case BulkActionType.enable:
case BulkActionTypeEnum.enable:
return i18n.RULES_BULK_ENABLE_SUCCESS_DESCRIPTION(summary.succeeded);
case BulkActionType.disable:
case BulkActionTypeEnum.disable:
return i18n.RULES_BULK_DISABLE_SUCCESS_DESCRIPTION(summary.succeeded);
}
}
@ -67,9 +70,9 @@ export function explainBulkEditSuccess(
if (
editPayload.some(
(x) =>
x.type === BulkActionEditType.add_index_patterns ||
x.type === BulkActionEditType.set_index_patterns ||
x.type === BulkActionEditType.delete_index_patterns
x.type === BulkActionEditTypeEnum.add_index_patterns ||
x.type === BulkActionEditTypeEnum.set_index_patterns ||
x.type === BulkActionEditTypeEnum.delete_index_patterns
)
) {
return `${i18n.RULES_BULK_EDIT_SUCCESS_DESCRIPTION(
@ -83,22 +86,22 @@ export function explainBulkEditSuccess(
export function summarizeBulkError(action: BulkActionType): string {
switch (action) {
case BulkActionType.export:
case BulkActionTypeEnum.export:
return i18n.RULES_BULK_EXPORT_FAILURE;
case BulkActionType.duplicate:
case BulkActionTypeEnum.duplicate:
return i18n.RULES_BULK_DUPLICATE_FAILURE;
case BulkActionType.delete:
case BulkActionTypeEnum.delete:
return i18n.RULES_BULK_DELETE_FAILURE;
case BulkActionType.enable:
case BulkActionTypeEnum.enable:
return i18n.RULES_BULK_ENABLE_FAILURE;
case BulkActionType.disable:
case BulkActionTypeEnum.disable:
return i18n.RULES_BULK_DISABLE_FAILURE;
case BulkActionType.edit:
case BulkActionTypeEnum.edit:
return i18n.RULES_BULK_EDIT_FAILURE;
}
}
@ -112,22 +115,22 @@ export function explainBulkError(action: BulkActionType, error: HTTPError): stri
}
switch (action) {
case BulkActionType.export:
case BulkActionTypeEnum.export:
return i18n.RULES_BULK_EXPORT_FAILURE_DESCRIPTION(summary.failed);
case BulkActionType.duplicate:
case BulkActionTypeEnum.duplicate:
return i18n.RULES_BULK_DUPLICATE_FAILURE_DESCRIPTION(summary.failed);
case BulkActionType.delete:
case BulkActionTypeEnum.delete:
return i18n.RULES_BULK_DELETE_FAILURE_DESCRIPTION(summary.failed);
case BulkActionType.enable:
case BulkActionTypeEnum.enable:
return i18n.RULES_BULK_ENABLE_FAILURE_DESCRIPTION(summary.failed);
case BulkActionType.disable:
case BulkActionTypeEnum.disable:
return i18n.RULES_BULK_DISABLE_FAILURE_DESCRIPTION(summary.failed);
case BulkActionType.edit:
case BulkActionTypeEnum.edit:
return i18n.RULES_BULK_EDIT_FAILURE_DESCRIPTION(summary.failed, summary.skipped);
}
}

View file

@ -6,7 +6,7 @@
*/
import { renderHook } from '@testing-library/react-hooks';
import { BulkActionType } from '../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionTypeEnum } from '../../../../../common/api/detection_engine/rule_management';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { useRulesTableContextOptional } from '../../../rule_management_ui/components/rules_table/rules_table/rules_table_context';
import { useBulkExportMutation } from '../../api/hooks/use_bulk_export_mutation';
@ -92,7 +92,7 @@ describe('useBulkExport', () => {
expect(setLoadingRules).toHaveBeenCalledWith({
ids: ['ruleId1', 'ruleId2'],
action: BulkActionType.export,
action: BulkActionTypeEnum.export,
});
});
@ -101,7 +101,7 @@ describe('useBulkExport', () => {
expect(setLoadingRules).toHaveBeenCalledWith({
ids: [],
action: BulkActionType.export,
action: BulkActionTypeEnum.export,
});
});

View file

@ -6,7 +6,7 @@
*/
import { useCallback } from 'react';
import { BulkActionType } from '../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionTypeEnum } from '../../../../../common/api/detection_engine/rule_management';
import { useRulesTableContextOptional } from '../../../rule_management_ui/components/rules_table/rules_table/rules_table_context';
import { useBulkExportMutation } from '../../api/hooks/use_bulk_export_mutation';
import { useShowBulkErrorToast } from './use_show_bulk_error_toast';
@ -24,12 +24,12 @@ export function useBulkExport() {
async (queryOrIds: QueryOrIds) => {
try {
setLoadingRules?.({
ids: queryOrIds.ids ?? guessRuleIdsForBulkAction(BulkActionType.export),
action: BulkActionType.export,
ids: queryOrIds.ids ?? guessRuleIdsForBulkAction(BulkActionTypeEnum.export),
action: BulkActionTypeEnum.export,
});
return await mutateAsync(queryOrIds);
} catch (error) {
showBulkErrorToast({ actionType: BulkActionType.export, error });
showBulkErrorToast({ actionType: BulkActionTypeEnum.export, error });
} finally {
setLoadingRules?.({ ids: [], action: null });
}

View file

@ -6,7 +6,7 @@
*/
import { useCallback } from 'react';
import { BulkActionType } from '../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionTypeEnum } from '../../../../../common/api/detection_engine/rule_management';
import { downloadBlob } from '../../../../common/utils/download_blob';
import * as i18n from '../../../../detections/pages/detection_engine/rules/translations';
import { getExportedRulesCounts } from '../../../rule_management_ui/components/rules_table/helpers';
@ -27,11 +27,11 @@ export function useDownloadExportedRules() {
try {
downloadBlob(response, DEFAULT_EXPORT_FILENAME);
showBulkSuccessToast({
actionType: BulkActionType.export,
actionType: BulkActionTypeEnum.export,
summary: await getExportedRulesCounts(response),
});
} catch (error) {
showBulkErrorToast({ actionType: BulkActionType.export, error });
showBulkErrorToast({ actionType: BulkActionTypeEnum.export, error });
}
},
[showBulkSuccessToast, showBulkErrorToast]

View file

@ -6,7 +6,7 @@
*/
import { renderHook } from '@testing-library/react-hooks';
import { BulkActionType } from '../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionTypeEnum } from '../../../../../common/api/detection_engine/rule_management';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../../../common/lib/telemetry';
import { useBulkActionMutation } from '../../api/hooks/use_bulk_action_mutation';
@ -61,7 +61,7 @@ describe('useExecuteBulkAction', () => {
it('executes bulk action', async () => {
const bulkAction = {
type: BulkActionType.enable,
type: BulkActionTypeEnum.enable,
query: 'some query',
} as const;
@ -73,7 +73,7 @@ describe('useExecuteBulkAction', () => {
describe('state handlers', () => {
it('shows success toast upon completion', async () => {
await executeBulkAction({
type: BulkActionType.enable,
type: BulkActionTypeEnum.enable,
ids: ['ruleId1'],
});
@ -84,7 +84,7 @@ describe('useExecuteBulkAction', () => {
it('does not shows success toast upon completion if suppressed', async () => {
await executeBulkAction(
{
type: BulkActionType.enable,
type: BulkActionTypeEnum.enable,
ids: ['ruleId1'],
},
{ suppressSuccessToast: true }
@ -100,7 +100,7 @@ describe('useExecuteBulkAction', () => {
});
await executeBulkAction({
type: BulkActionType.enable,
type: BulkActionTypeEnum.enable,
ids: ['ruleId1'],
});
@ -126,31 +126,31 @@ describe('useExecuteBulkAction', () => {
it('sets the loading state before execution', async () => {
await executeBulkAction({
type: BulkActionType.enable,
type: BulkActionTypeEnum.enable,
ids: ['ruleId1', 'ruleId2'],
});
expect(setLoadingRules).toHaveBeenCalledWith({
ids: ['ruleId1', 'ruleId2'],
action: BulkActionType.enable,
action: BulkActionTypeEnum.enable,
});
});
it('sets the empty loading state before execution when query is set', async () => {
await executeBulkAction({
type: BulkActionType.enable,
type: BulkActionTypeEnum.enable,
query: 'some query',
});
expect(setLoadingRules).toHaveBeenCalledWith({
ids: [],
action: BulkActionType.enable,
action: BulkActionTypeEnum.enable,
});
});
it('clears loading state for the processing rules after execution', async () => {
await executeBulkAction({
type: BulkActionType.enable,
type: BulkActionTypeEnum.enable,
ids: ['ruleId1', 'ruleId2'],
});
@ -163,7 +163,7 @@ describe('useExecuteBulkAction', () => {
});
await executeBulkAction({
type: BulkActionType.enable,
type: BulkActionTypeEnum.enable,
ids: ['ruleId1', 'ruleId2'],
});
@ -174,7 +174,7 @@ describe('useExecuteBulkAction', () => {
describe('telemetry', () => {
it('sends for enable action', async () => {
await executeBulkAction({
type: BulkActionType.enable,
type: BulkActionTypeEnum.enable,
query: 'some query',
});
@ -184,7 +184,7 @@ describe('useExecuteBulkAction', () => {
it('sends for disable action', async () => {
await executeBulkAction({
type: BulkActionType.disable,
type: BulkActionTypeEnum.disable,
query: 'some query',
});

View file

@ -9,7 +9,8 @@ import type { NavigateToAppOptions } from '@kbn/core/public';
import { useCallback } from 'react';
import type { BulkActionResponse } from '..';
import { APP_UI_ID } from '../../../../../common/constants';
import { BulkActionType } from '../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import type { BulkActionType } from '../../../../../common/api/detection_engine/rule_management';
import { BulkActionTypeEnum } from '../../../../../common/api/detection_engine/rule_management';
import { SecurityPageName } from '../../../../app/types';
import { getEditRuleUrl } from '../../../../common/components/link_to/redirect_to_detection_engine';
import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../../../common/lib/telemetry';
@ -58,7 +59,7 @@ export const useExecuteBulkAction = (options?: UseExecuteBulkActionOptions) => {
actionType: bulkAction.type,
summary: response.attributes.summary,
editPayload:
bulkAction.type === BulkActionType.edit ? bulkAction.editPayload : undefined,
bulkAction.type === BulkActionTypeEnum.edit ? bulkAction.editPayload : undefined,
});
}
@ -83,14 +84,14 @@ export const useExecuteBulkAction = (options?: UseExecuteBulkActionOptions) => {
};
function sendTelemetry(action: BulkActionType, response: BulkActionResponse): void {
if (action !== BulkActionType.disable && action !== BulkActionType.enable) {
if (action !== BulkActionTypeEnum.disable && action !== BulkActionTypeEnum.enable) {
return;
}
if (response.attributes.results.updated.some((rule) => rule.immutable)) {
track(
METRIC_TYPE.COUNT,
action === BulkActionType.enable
action === BulkActionTypeEnum.enable
? TELEMETRY_EVENT.SIEM_RULE_ENABLED
: TELEMETRY_EVENT.SIEM_RULE_DISABLED
);
@ -99,7 +100,7 @@ function sendTelemetry(action: BulkActionType, response: BulkActionResponse): vo
if (response.attributes.results.updated.some((rule) => !rule.immutable)) {
track(
METRIC_TYPE.COUNT,
action === BulkActionType.disable
action === BulkActionTypeEnum.disable
? TELEMETRY_EVENT.CUSTOM_RULE_DISABLED
: TELEMETRY_EVENT.CUSTOM_RULE_ENABLED
);

View file

@ -6,7 +6,8 @@
*/
import { useCallback } from 'react';
import { BulkActionType } from '../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import type { BulkActionType } from '../../../../../common/api/detection_engine/rule_management';
import { BulkActionTypeEnum } from '../../../../../common/api/detection_engine/rule_management';
import { useRulesTableContextOptional } from '../../../rule_management_ui/components/rules_table/rules_table/rules_table_context';
export function useGuessRuleIdsForBulkAction(): (bulkActionType: BulkActionType) => string[] {
@ -16,9 +17,9 @@ export function useGuessRuleIdsForBulkAction(): (bulkActionType: BulkActionType)
(bulkActionType: BulkActionType) => {
const allRules = rulesTableContext?.state.isAllSelected ? rulesTableContext.state.rules : [];
const processingRules =
bulkActionType === BulkActionType.enable
bulkActionType === BulkActionTypeEnum.enable
? allRules.filter((x) => !x.enabled)
: bulkActionType === BulkActionType.disable
: bulkActionType === BulkActionTypeEnum.disable
? allRules.filter((x) => x.enabled)
: allRules;

View file

@ -8,7 +8,7 @@
import { useCallback } from 'react';
import type { HTTPError } from '../../../../../common/detection_engine/types';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import type { BulkActionType } from '../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import type { BulkActionType } from '../../../../../common/api/detection_engine/rule_management';
import { explainBulkError, summarizeBulkError } from './translations';
interface ShowBulkErrorToastProps {

View file

@ -8,8 +8,11 @@
import { useCallback } from 'react';
import type { BulkActionSummary } from '..';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import type { BulkActionEditPayload } from '../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionType } from '../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import type {
BulkActionEditPayload,
BulkActionType,
} from '../../../../../common/api/detection_engine/rule_management';
import { BulkActionTypeEnum } from '../../../../../common/api/detection_engine/rule_management';
import { explainBulkEditSuccess, explainBulkSuccess, summarizeBulkSuccess } from './translations';
interface ShowBulkSuccessToastProps {
@ -24,7 +27,7 @@ export function useShowBulkSuccessToast() {
return useCallback(
({ actionType, summary, editPayload }: ShowBulkSuccessToastProps) => {
const text =
actionType === BulkActionType.edit
actionType === BulkActionTypeEnum.edit
? explainBulkEditSuccess(editPayload ?? [], summary)
: explainBulkSuccess(actionType, summary);

View file

@ -5,12 +5,11 @@
* 2.0.
*/
import * as t from 'io-ts';
import * as z from 'zod';
import type { RuleSnooze } from '@kbn/alerting-plugin/common';
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types';
import { PositiveInteger } from '@kbn/securitysolution-io-ts-types';
import type { RuleSnoozeSettings } from '@kbn/triggers-actions-ui-plugin/public/types';
import type { WarningSchema } from '../../../../common/api/detection_engine';
import type { RuleExecutionStatus } from '../../../../common/api/detection_engine/rule_monitoring';
@ -49,11 +48,11 @@ export interface PatchRuleProps {
export type Rule = RuleResponse;
export type PaginationOptions = t.TypeOf<typeof PaginationOptions>;
export const PaginationOptions = t.type({
page: PositiveInteger,
perPage: PositiveInteger,
total: PositiveInteger,
export type PaginationOptions = z.infer<typeof PaginationOptions>;
export const PaginationOptions = z.object({
page: z.number().int().min(0),
perPage: z.number().int().min(0),
total: z.number().int().min(0),
});
export interface FetchRulesProps {
@ -81,8 +80,8 @@ export interface RulesSnoozeSettingsBatchResponse {
data: RuleSnoozeSettingsResponse[];
}
export type SortingOptions = t.TypeOf<typeof SortingOptions>;
export const SortingOptions = t.type({
export type SortingOptions = z.infer<typeof SortingOptions>;
export const SortingOptions = z.object({
field: FindRulesSortField,
order: SortOrder,
});

View file

@ -10,7 +10,7 @@ import { EuiConfirmModal } from '@elastic/eui';
import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations';
import { BulkActionRuleErrorsList } from './bulk_action_rule_errors_list';
import { BulkActionType } from '../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionTypeEnum } from '../../../../../../common/api/detection_engine/rule_management';
import { assertUnreachable } from '../../../../../../common/utility_types';
import type { BulkActionForConfirmation, DryRunResult } from './types';
@ -20,9 +20,9 @@ const getActionRejectedTitle = (
failedRulesCount: number
) => {
switch (bulkAction) {
case BulkActionType.edit:
case BulkActionTypeEnum.edit:
return i18n.BULK_EDIT_CONFIRMATION_REJECTED_TITLE(failedRulesCount);
case BulkActionType.export:
case BulkActionTypeEnum.export:
return i18n.BULK_EXPORT_CONFIRMATION_REJECTED_TITLE(failedRulesCount);
default:
assertUnreachable(bulkAction);
@ -34,9 +34,9 @@ const getActionConfirmLabel = (
succeededRulesCount: number
) => {
switch (bulkAction) {
case BulkActionType.edit:
case BulkActionTypeEnum.edit:
return i18n.BULK_EDIT_CONFIRMATION_CONFIRM(succeededRulesCount);
case BulkActionType.export:
case BulkActionTypeEnum.export:
return i18n.BULK_EXPORT_CONFIRMATION_CONFIRM(succeededRulesCount);
default:
assertUnreachable(bulkAction);

View file

@ -13,7 +13,7 @@ import { render, screen } from '@testing-library/react';
import { BulkActionRuleErrorsList } from './bulk_action_rule_errors_list';
import { BulkActionsDryRunErrCode } from '../../../../../../common/constants';
import type { DryRunResult } from './types';
import { BulkActionType } from '../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionTypeEnum } from '../../../../../../common/api/detection_engine/rule_management';
const Wrapper: FC = ({ children }) => {
return (
@ -26,7 +26,7 @@ const Wrapper: FC = ({ children }) => {
describe('Component BulkEditRuleErrorsList', () => {
test('should not render component if no errors present', () => {
const { container } = render(
<BulkActionRuleErrorsList bulkAction={BulkActionType.edit} ruleErrors={[]} />,
<BulkActionRuleErrorsList bulkAction={BulkActionTypeEnum.edit} ruleErrors={[]} />,
{
wrapper: Wrapper,
}
@ -46,9 +46,12 @@ describe('Component BulkEditRuleErrorsList', () => {
ruleIds: ['rule:1'],
},
];
render(<BulkActionRuleErrorsList bulkAction={BulkActionType.edit} ruleErrors={ruleErrors} />, {
wrapper: Wrapper,
});
render(
<BulkActionRuleErrorsList bulkAction={BulkActionTypeEnum.edit} ruleErrors={ruleErrors} />,
{
wrapper: Wrapper,
}
);
expect(screen.getByText("2 rules can't be edited (test failure)")).toBeInTheDocument();
expect(screen.getByText("1 rule can't be edited (another failure)")).toBeInTheDocument();
@ -80,9 +83,12 @@ describe('Component BulkEditRuleErrorsList', () => {
ruleIds: ['rule:1', 'rule:2'],
},
];
render(<BulkActionRuleErrorsList bulkAction={BulkActionType.edit} ruleErrors={ruleErrors} />, {
wrapper: Wrapper,
});
render(
<BulkActionRuleErrorsList bulkAction={BulkActionTypeEnum.edit} ruleErrors={ruleErrors} />,
{
wrapper: Wrapper,
}
);
expect(screen.getByText(value)).toBeInTheDocument();
});

View file

@ -10,7 +10,7 @@ import { EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { BulkActionsDryRunErrCode } from '../../../../../../common/constants';
import { BulkActionType } from '../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionTypeEnum } from '../../../../../../common/api/detection_engine/rule_management';
import type { DryRunResult, BulkActionForConfirmation } from './types';
@ -132,7 +132,7 @@ const BulkActionRuleErrorsListComponent = ({
{ruleErrors.map(({ message, errorCode, ruleIds }) => {
const rulesCount = ruleIds.length;
switch (bulkAction) {
case BulkActionType.edit:
case BulkActionTypeEnum.edit:
return (
<BulkEditRuleErrorItem
message={message}
@ -141,7 +141,7 @@ const BulkActionRuleErrorsListComponent = ({
/>
);
case BulkActionType.export:
case BulkActionTypeEnum.export:
return (
<BulkExportRuleErrorItem
message={message}

View file

@ -7,8 +7,11 @@
import React from 'react';
import type { BulkActionEditPayload } from '../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionEditType } from '../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import type {
BulkActionEditPayload,
BulkActionEditType,
} from '../../../../../../common/api/detection_engine/rule_management';
import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management';
import { IndexPatternsForm } from './forms/index_patterns_form';
import { TagsForm } from './forms/tags_form';
@ -25,23 +28,23 @@ interface BulkEditFlyoutProps {
const BulkEditFlyoutComponent = ({ editAction, ...props }: BulkEditFlyoutProps) => {
switch (editAction) {
case BulkActionEditType.add_index_patterns:
case BulkActionEditType.delete_index_patterns:
case BulkActionEditType.set_index_patterns:
case BulkActionEditTypeEnum.add_index_patterns:
case BulkActionEditTypeEnum.delete_index_patterns:
case BulkActionEditTypeEnum.set_index_patterns:
return <IndexPatternsForm {...props} editAction={editAction} />;
case BulkActionEditType.add_tags:
case BulkActionEditType.delete_tags:
case BulkActionEditType.set_tags:
case BulkActionEditTypeEnum.add_tags:
case BulkActionEditTypeEnum.delete_tags:
case BulkActionEditTypeEnum.set_tags:
return <TagsForm {...props} editAction={editAction} />;
case BulkActionEditType.set_timeline:
case BulkActionEditTypeEnum.set_timeline:
return <TimelineTemplateForm {...props} />;
case BulkActionEditType.add_rule_actions:
case BulkActionEditType.set_rule_actions:
case BulkActionEditTypeEnum.add_rule_actions:
case BulkActionEditTypeEnum.set_rule_actions:
return <RuleActionsForm {...props} />;
case BulkActionEditType.set_schedule:
case BulkActionEditTypeEnum.set_schedule:
return <ScheduleForm {...props} />;
default:

View file

@ -14,8 +14,8 @@ import * as i18n from '../../../../../../detections/pages/detection_engine/rules
import { DEFAULT_INDEX_KEY } from '../../../../../../../common/constants';
import { useKibana } from '../../../../../../common/lib/kibana';
import { BulkActionEditType } from '../../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import type { BulkActionEditPayload } from '../../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionEditTypeEnum } from '../../../../../../../common/api/detection_engine/rule_management';
import type { BulkActionEditPayload } from '../../../../../../../common/api/detection_engine/rule_management';
import type { FormSchema } from '../../../../../../shared_imports';
import {
@ -31,9 +31,9 @@ import { BulkEditFormWrapper } from './bulk_edit_form_wrapper';
const CommonUseField = getUseField({ component: Field });
type IndexPatternsEditActions =
| BulkActionEditType.add_index_patterns
| BulkActionEditType.delete_index_patterns
| BulkActionEditType.set_index_patterns;
| BulkActionEditTypeEnum['add_index_patterns']
| BulkActionEditTypeEnum['delete_index_patterns']
| BulkActionEditTypeEnum['set_index_patterns'];
interface IndexPatternsFormData {
index: string[];
@ -70,7 +70,7 @@ const initialFormData: IndexPatternsFormData = {
};
const getFormConfig = (editAction: IndexPatternsEditActions) =>
editAction === BulkActionEditType.add_index_patterns
editAction === BulkActionEditTypeEnum.add_index_patterns
? {
indexLabel: i18n.BULK_EDIT_FLYOUT_FORM_ADD_INDEX_PATTERNS_LABEL,
indexHelpText: i18n.BULK_EDIT_FLYOUT_FORM_ADD_INDEX_PATTERNS_HELP_TEXT,
@ -115,13 +115,11 @@ const IndexPatternsFormComponent = ({
return;
}
const payload = {
onConfirm({
value: data.index,
type: data.overwrite ? BulkActionEditType.set_index_patterns : editAction,
type: data.overwrite ? BulkActionEditTypeEnum.set_index_patterns : editAction,
overwrite_data_views: data.overwriteDataViews,
};
onConfirm(payload);
});
};
return (
@ -140,7 +138,7 @@ const IndexPatternsFormComponent = ({
},
}}
/>
{editAction === BulkActionEditType.add_index_patterns && (
{editAction === BulkActionEditTypeEnum.add_index_patterns && (
<CommonUseField
path="overwrite"
componentProps={{
@ -161,7 +159,7 @@ const IndexPatternsFormComponent = ({
</EuiCallOut>
</EuiFormRow>
)}
{editAction === BulkActionEditType.add_index_patterns && (
{editAction === BulkActionEditTypeEnum.add_index_patterns && (
<CommonUseField
path="overwriteDataViews"
componentProps={{
@ -180,7 +178,7 @@ const IndexPatternsFormComponent = ({
</EuiCallOut>
</EuiFormRow>
)}
{editAction === BulkActionEditType.delete_index_patterns && (
{editAction === BulkActionEditTypeEnum.delete_index_patterns && (
<EuiFormRow fullWidth>
<EuiCallOut color="warning" size="s" data-test-subj="bulkEditRulesDataViewsWarning">
<FormattedMessage

View file

@ -23,8 +23,8 @@ import {
getUseField,
Field,
} from '../../../../../../shared_imports';
import { BulkActionEditType } from '../../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import type { BulkActionEditPayload } from '../../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionEditTypeEnum } from '../../../../../../../common/api/detection_engine/rule_management';
import type { BulkActionEditPayload } from '../../../../../../../common/api/detection_engine/rule_management';
import { BulkEditFormWrapper } from './bulk_edit_form_wrapper';
import { bulkAddRuleActions as i18n } from '../translations';
@ -99,8 +99,8 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction
const { actions = [], overwrite: overwriteValue } = data;
const editAction = overwriteValue
? BulkActionEditType.set_rule_actions
: BulkActionEditType.add_rule_actions;
? BulkActionEditTypeEnum.set_rule_actions
: BulkActionEditTypeEnum.add_rule_actions;
onConfirm({
type: editAction,

View file

@ -7,8 +7,8 @@
import { EuiCallOut } from '@elastic/eui';
import React, { useCallback } from 'react';
import type { BulkActionEditPayload } from '../../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionEditType } from '../../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import type { BulkActionEditPayload } from '../../../../../../../common/api/detection_engine/rule_management';
import { BulkActionEditTypeEnum } from '../../../../../../../common/api/detection_engine/rule_management';
import { ScheduleItem } from '../../../../../../detections/components/rules/schedule_item_form';
import type { FormSchema } from '../../../../../../shared_imports';
import { UseField, useForm } from '../../../../../../shared_imports';
@ -55,7 +55,7 @@ export const ScheduleForm = ({ rulesCount, onClose, onConfirm }: ScheduleFormCom
}
onConfirm({
type: BulkActionEditType.set_schedule,
type: BulkActionEditTypeEnum.set_schedule,
value: {
interval: data.interval,
lookback: data.lookback,

View file

@ -10,8 +10,8 @@ import { FormattedMessage } from '@kbn/i18n-react';
import React, { useMemo } from 'react';
import { useRuleManagementFilters } from '../../../../../rule_management/logic/use_rule_management_filters';
import type { BulkActionEditPayload } from '../../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionEditType } from '../../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import type { BulkActionEditPayload } from '../../../../../../../common/api/detection_engine/rule_management';
import { BulkActionEditTypeEnum } from '../../../../../../../common/api/detection_engine/rule_management';
import * as i18n from '../../../../../../detections/pages/detection_engine/rules/translations';
import { caseInsensitiveSort } from '../../helpers';
@ -28,9 +28,9 @@ import {
import { BulkEditFormWrapper } from './bulk_edit_form_wrapper';
type TagsEditActions =
| BulkActionEditType.add_tags
| BulkActionEditType.delete_tags
| BulkActionEditType.set_tags;
| BulkActionEditTypeEnum['add_tags']
| BulkActionEditTypeEnum['delete_tags']
| BulkActionEditTypeEnum['set_tags'];
const CommonUseField = getUseField({ component: Field });
@ -58,7 +58,7 @@ const schema: FormSchema<TagsFormData> = {
const initialFormData: TagsFormData = { tags: [], overwrite: false };
const getFormConfig = (editAction: TagsEditActions) =>
editAction === BulkActionEditType.add_tags
editAction === BulkActionEditTypeEnum.add_tags
? {
tagsLabel: i18n.BULK_EDIT_FLYOUT_FORM_ADD_TAGS_LABEL,
tagsHelpText: i18n.BULK_EDIT_FLYOUT_FORM_ADD_TAGS_HELP_TEXT,
@ -97,12 +97,10 @@ const TagsFormComponent = ({ editAction, rulesCount, onClose, onConfirm }: TagsF
return;
}
const payload = {
onConfirm({
value: data.tags,
type: data.overwrite ? BulkActionEditType.set_tags : editAction,
};
onConfirm(payload);
type: data.overwrite ? BulkActionEditTypeEnum.set_tags : editAction,
});
};
return (
@ -121,7 +119,7 @@ const TagsFormComponent = ({ editAction, rulesCount, onClose, onConfirm }: TagsF
},
}}
/>
{editAction === BulkActionEditType.add_tags ? (
{editAction === BulkActionEditTypeEnum.add_tags ? (
<CommonUseField
path="overwrite"
componentProps={{

View file

@ -11,8 +11,8 @@ import { EuiCallOut } from '@elastic/eui';
import type { FormSchema } from '../../../../../../shared_imports';
import { useForm, UseField } from '../../../../../../shared_imports';
import { PickTimeline } from '../../../../../../detections/components/rules/pick_timeline';
import type { BulkActionEditPayload } from '../../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import { BulkActionEditType } from '../../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import type { BulkActionEditPayload } from '../../../../../../../common/api/detection_engine/rule_management';
import { BulkActionEditTypeEnum } from '../../../../../../../common/api/detection_engine/rule_management';
import { BulkEditFormWrapper } from './bulk_edit_form_wrapper';
import { bulkApplyTimelineTemplate as i18n } from '../translations';
@ -62,7 +62,7 @@ const TimelineTemplateFormComponent = (props: TimelineTemplateFormProps) => {
const timelineTitle = timelineId ? data.timeline.title : '';
onConfirm({
type: BulkActionEditType.set_timeline,
type: BulkActionEditTypeEnum.set_timeline,
value: {
timeline_id: timelineId,
timeline_title: timelineTitle,

View file

@ -6,14 +6,14 @@
*/
import type { BulkActionsDryRunErrCode } from '../../../../../../common/constants';
import type { BulkActionType } from '../../../../../../common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route';
import type { BulkActionTypeEnum } from '../../../../../../common/api/detection_engine/rule_management';
/**
* Only 2 bulk actions are supported for for confirmation dry run modal:
* * export
* * edit
*/
export type BulkActionForConfirmation = BulkActionType.export | BulkActionType.edit;
export type BulkActionForConfirmation = BulkActionTypeEnum['export'] | BulkActionTypeEnum['edit'];
/**
* transformed results of dry run

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