mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Detections] Extended rule execution logging to Event Log (#126063)
**Epics:** https://github.com/elastic/kibana/issues/124947, https://github.com/elastic/kibana/issues/118324 **Fixes:** https://github.com/elastic/kibana/issues/131352 ## Summary Console logs written by rule executors can now be "routed" to the Event Log in addition to the console. A new table UI for viewing plain rule execution logs allows the user to look at all status changes, errors, warnings, info and debug messages on the Rule Details page. <img width="1502" alt="Screenshot 2022-07-20 at 15 31 54" src="https://user-images.githubusercontent.com/7359339/179995075-24440224-daf9-4e73-bc62-b6ce211052b3.png"> **This feature is hidden under a feature flag and disabled by default** -- it might not be production-ready yet. We will need to work on product and UX/UI design in the subsequent development cycles to make it ready for release. Until then, we can start using it in development: it should make it easier to troubleshoot issues with rule execution. Add this flag to your Kibana config to enable this feature: ```yaml xpack.securitySolution.enableExperimental: ['extendedRuleExecutionLoggingEnabled'] ``` If the flag is enabled: - Rules will start writing console logs to Event Log as events of a new type `message`, in addition to the existing `status-change` and `execution-metrics` events. - Rule Details page will show a new tab called `Execution events`. This tab will contain a table with plain execution logs. - In Stack Management, you will find two new Kibana Advanced Settings for controlling this extended logging. As for the new Kibana Advanced Settings, by default: - Extended logging is enabled. - The minimum console log level to be written to Event Log is `error`. This only affects the new `message` events. <img width="774" alt="Screenshot 2022-07-20 at 15 41 29" src="https://user-images.githubusercontent.com/7359339/179997070-d86dfc6b-3862-49ff-879d-ecc30bc128d7.png"> ## Implementation details **Important change**: refactored the folder structure as our first step to **domain-driven architecture** and **splitting the Detection Engine into subdomains**. - Extracted most of the code related to Rule Execution Log and Rule Monitoring in general into a subdomain called `rule_monitoring`. This subdomain now lives in three folders: - `security_solution/common/detection_engine/rule_monitoring` - `security_solution/public/detection_engine/rule_monitoring` - `security_solution/server/lib/detection_engine/rule_monitoring` - Tried to create a developer-friendly and clear folder structure within the subdomain. Other changes: - Changed all rule executors to write console logs via an instance of `IRuleExecutionLogForExecutors` instead of the console `Logger`. - `IRuleExecutionLogForExecutors` is passed to rule executors and downstream functions they call. - `Logger` is not passed anymore. - `buildRuleMessage` and `buildRuleMessageFactory` are deleted. - Added support for writing console logs to Event Log. - Added a new rule execution event type `message` for writing console logs to Event Log. - Every rule execution event now has a `log.level` and `event.severity`. - Improved the format of console logs written by rules. - Created a child logger for console logs of rule executors: `plugins.securitySolution.ruleExecution`. - Added rule static “signature” ID (`rule.rule_id`) as a correlation id to the logs. - Cleaned up the formatting of console logs. - Fixed `ExtMeta` to use interfaces instead of type intersection due to a found [bug](https://github.com/microsoft/TypeScript/issues/47935) in TypeScript that affected this type. - Made changes in the domain model. - Renamed the `AggregateRuleExecutionEvent` into the `RuleExecutionResult`. - The new plain event is called `RuleExecutionEvent`. - Finalized the API endpoint for fetching plain execution logs. - Built a Rule Execution Events Table UI for showing and filtering plain execution logs. - Did some refactoring to extract reusable components/hooks to make development of tables easier in the future. ## Execution events table UI For context, this is how the existing `Execution logs` table looks like when the flag is off (notice the renaming to `Execution results`: <img width="1506" alt="Screenshot 2022-07-20 at 15 29 04" src="https://user-images.githubusercontent.com/7359339/179994450-45121035-ebb0-4e6f-83c0-9cbbbbd0b598.png"> This is the new `Execution events` table when the flag is on: <img width="1502" alt="Screenshot 2022-07-20 at 15 31 54" src="https://user-images.githubusercontent.com/7359339/179995075-24440224-daf9-4e73-bc62-b6ce211052b3.png"> Showing only trace and debug events: <img width="1505" alt="Screenshot 2022-07-20 at 15 33 48" src="https://user-images.githubusercontent.com/7359339/179995484-d97ff7e3-2756-42db-802f-41f11bd37507.png"> Showing only status changes: <img width="1507" alt="Screenshot 2022-07-20 at 15 35 04" src="https://user-images.githubusercontent.com/7359339/179995804-ca6808b7-3b47-411b-a74e-d141b3fd74e0.png"> Showing only warning and error `message`s: <img width="1508" alt="Screenshot 2022-07-20 at 15 37 11" src="https://user-images.githubusercontent.com/7359339/179996258-c154b95d-642d-45a6-b19a-7185cd71f295.png"> Expanded rows showing details of the corresponding events: <img width="1452" alt="Screenshot 2022-07-20 at 15 39 16" src="https://user-images.githubusercontent.com/7359339/179996771-3954ceea-24e9-4760-9103-2daf6cb7b528.png"> <img width="1449" alt="Screenshot 2022-07-20 at 15 39 56" src="https://user-images.githubusercontent.com/7359339/179996805-c866674d-09a1-42ec-b954-58c6829ef19b.png"> ## Console logs Example: ``` [2022-02-23T17:05:09.901+03:00][DEBUG][plugins.securitySolution.ruleExecution] [+] Starting Signal Rule execution [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] [2022-02-23T17:05:09.907+03:00][DEBUG][plugins.securitySolution.ruleExecution] interval: 5m [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] [2022-02-23T17:05:09.908+03:00][INFO ][plugins.securitySolution.ruleExecution] Changing rule status to "running" [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] [2022-02-23T17:05:10.595+03:00][WARN ][plugins.securitySolution.ruleExecution] This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent. [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] [2022-02-23T17:05:10.595+03:00][WARN ][plugins.securitySolution.ruleExecution] Changing rule status to "partial failure" [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] [2022-02-23T17:05:11.630+03:00][DEBUG][plugins.securitySolution.ruleExecution] sortIds: undefined [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] [2022-02-23T17:05:11.634+03:00][DEBUG][plugins.securitySolution.ruleExecution] totalHits: 0 [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] [2022-02-23T17:05:11.634+03:00][DEBUG][plugins.securitySolution.ruleExecution] searchResult.hit.hits.length: 0 [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] [2022-02-23T17:05:11.635+03:00][DEBUG][plugins.securitySolution.ruleExecution] totalHits was 0, exiting early [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] [2022-02-23T17:05:11.636+03:00][DEBUG][plugins.securitySolution.ruleExecution] [+] completed bulk index of 0 [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] [2022-02-23T17:05:11.636+03:00][DEBUG][plugins.securitySolution.ruleExecution] [+] Signal Rule execution completed. [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] [2022-02-23T17:05:11.638+03:00][DEBUG][plugins.securitySolution.ruleExecution] [+] Finished indexing 0 signals into .alerts-security.alerts [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] [2022-02-23T17:05:11.639+03:00][DEBUG][plugins.securitySolution.ruleExecution] [+] Finished indexing 0 signals searched between date ranges [ { "to": "2022-02-23T14:05:09.775Z", "from": "2022-02-23T13:55:09.775Z", "maxSignals": 10000 } ] [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] ``` Note that: - The logger name is now `plugins.securitySolution.ruleExecution`, which allows to turn on _only_ rule execution logs in the config (could be useful when debugging). - Every log message has a suffix with correlation ids: `[siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default]` ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] `x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/README.md` - [x] Various JSDoc comments - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
parent
45db88e0e6
commit
becaec81e1
239 changed files with 6524 additions and 3099 deletions
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
* 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 t from 'io-ts';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { defaultCsvArray } from '.';
|
||||
|
||||
describe('defaultCsvArray', () => {
|
||||
describe('Creates a schema of an array that works in the following way:', () => {
|
||||
type TestType = t.TypeOf<typeof TestType>;
|
||||
const TestType = t.union(
|
||||
[t.literal('foo'), t.literal('bar'), t.literal('42'), t.null, t.undefined],
|
||||
'TestType'
|
||||
);
|
||||
|
||||
const TestCsvArray = defaultCsvArray(TestType);
|
||||
|
||||
describe('Name of the schema', () => {
|
||||
it('has a default value', () => {
|
||||
const CsvArray = defaultCsvArray(TestType);
|
||||
expect(CsvArray.name).toEqual('DefaultCsvArray<TestType>');
|
||||
});
|
||||
|
||||
it('can be overriden', () => {
|
||||
const CsvArray = defaultCsvArray(TestType, 'CustomName');
|
||||
expect(CsvArray.name).toEqual('CustomName');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation succeeds', () => {
|
||||
describe('when input is a single valid string value', () => {
|
||||
const cases = [{ input: 'foo' }, { input: 'bar' }, { input: '42' }];
|
||||
|
||||
cases.forEach(({ input }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = TestCsvArray.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
const expectedOutput = [input]; // note that it's an array after decode
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when input is an array of valid string values', () => {
|
||||
const cases = [
|
||||
{ input: ['foo'] },
|
||||
{ input: ['foo', 'bar'] },
|
||||
{ input: ['foo', 'bar', '42'] },
|
||||
];
|
||||
|
||||
cases.forEach(({ input }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = TestCsvArray.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
const expectedOutput = input;
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when input is a string which is a comma-separated array of valid values', () => {
|
||||
const cases = [
|
||||
{
|
||||
input: 'foo,bar',
|
||||
expectedOutput: ['foo', 'bar'],
|
||||
},
|
||||
{
|
||||
input: 'foo,bar,42',
|
||||
expectedOutput: ['foo', 'bar', '42'],
|
||||
},
|
||||
];
|
||||
|
||||
cases.forEach(({ input, expectedOutput }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = TestCsvArray.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation fails', () => {
|
||||
describe('when input is a single invalid value', () => {
|
||||
const cases = [
|
||||
{
|
||||
input: 'val',
|
||||
expectedErrors: ['Invalid value "val" supplied to "DefaultCsvArray<TestType>"'],
|
||||
},
|
||||
{
|
||||
input: '5',
|
||||
expectedErrors: ['Invalid value "5" supplied to "DefaultCsvArray<TestType>"'],
|
||||
},
|
||||
{
|
||||
input: 5,
|
||||
expectedErrors: ['Invalid value "5" supplied to "DefaultCsvArray<TestType>"'],
|
||||
},
|
||||
{
|
||||
input: {},
|
||||
expectedErrors: ['Invalid value "{}" supplied to "DefaultCsvArray<TestType>"'],
|
||||
},
|
||||
];
|
||||
|
||||
cases.forEach(({ input, expectedErrors }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = TestCsvArray.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual(expectedErrors);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when input is an array of invalid values', () => {
|
||||
const cases = [
|
||||
{
|
||||
input: ['value 1', 5],
|
||||
expectedErrors: [
|
||||
'Invalid value "value 1" supplied to "DefaultCsvArray<TestType>"',
|
||||
'Invalid value "5" supplied to "DefaultCsvArray<TestType>"',
|
||||
],
|
||||
},
|
||||
{
|
||||
input: ['value 1', 'foo'],
|
||||
expectedErrors: ['Invalid value "value 1" supplied to "DefaultCsvArray<TestType>"'],
|
||||
},
|
||||
{
|
||||
input: ['', 5, {}],
|
||||
expectedErrors: [
|
||||
'Invalid value "" supplied to "DefaultCsvArray<TestType>"',
|
||||
'Invalid value "5" supplied to "DefaultCsvArray<TestType>"',
|
||||
'Invalid value "{}" supplied to "DefaultCsvArray<TestType>"',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
cases.forEach(({ input, expectedErrors }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = TestCsvArray.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual(expectedErrors);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when input is a string which is a comma-separated array of invalid values', () => {
|
||||
const cases = [
|
||||
{
|
||||
input: 'value 1,5',
|
||||
expectedErrors: [
|
||||
'Invalid value "value 1" supplied to "DefaultCsvArray<TestType>"',
|
||||
'Invalid value "5" supplied to "DefaultCsvArray<TestType>"',
|
||||
],
|
||||
},
|
||||
{
|
||||
input: 'value 1,foo',
|
||||
expectedErrors: ['Invalid value "value 1" supplied to "DefaultCsvArray<TestType>"'],
|
||||
},
|
||||
{
|
||||
input: ',5,{}',
|
||||
expectedErrors: [
|
||||
'Invalid value "" supplied to "DefaultCsvArray<TestType>"',
|
||||
'Invalid value "5" supplied to "DefaultCsvArray<TestType>"',
|
||||
'Invalid value "{}" supplied to "DefaultCsvArray<TestType>"',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
cases.forEach(({ input, expectedErrors }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = TestCsvArray.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual(expectedErrors);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation returns default value (an empty array)', () => {
|
||||
describe('when input is', () => {
|
||||
const cases = [{ input: null }, { input: undefined }, { input: '' }, { input: [] }];
|
||||
|
||||
cases.forEach(({ input }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = TestCsvArray.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
const expectedOutput: string[] = [];
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 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 t from 'io-ts';
|
||||
import { Either } from 'fp-ts/lib/Either';
|
||||
|
||||
/**
|
||||
* Creates a schema of an array that works in the following way:
|
||||
* - If input is a CSV string, it will be parsed to an array which will be validated.
|
||||
* - If input is an array, each item is validated to match `itemSchema`.
|
||||
* - If input is a single string, it is validated to match `itemSchema`.
|
||||
* - If input is not specified, the result will be set to [] (empty array):
|
||||
* - null, undefined, empty string, empty array
|
||||
*
|
||||
* In all cases when an input is valid, the resulting decoded value will be an array,
|
||||
* either an empty one or containing valid items.
|
||||
*
|
||||
* @param itemSchema Schema of the array's items.
|
||||
* @param name (Optional) Name of the resulting schema.
|
||||
*/
|
||||
export const defaultCsvArray = <TItem>(
|
||||
itemSchema: t.Type<TItem>,
|
||||
name?: string
|
||||
): t.Type<TItem[]> => {
|
||||
return new t.Type<TItem[]>(
|
||||
name ?? `DefaultCsvArray<${itemSchema.name}>`,
|
||||
t.array(itemSchema).is,
|
||||
(input, context): Either<t.Errors, TItem[]> => {
|
||||
if (input == null) {
|
||||
return t.success([]);
|
||||
} else if (typeof input === 'string') {
|
||||
if (input === '') {
|
||||
return t.success([]);
|
||||
} else {
|
||||
return t.array(itemSchema).validate(input.split(','), context);
|
||||
}
|
||||
} else {
|
||||
return t.array(itemSchema).validate(input, context);
|
||||
}
|
||||
},
|
||||
t.identity
|
||||
);
|
||||
};
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 t from 'io-ts';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { defaultValue } from '.';
|
||||
|
||||
describe('defaultValue', () => {
|
||||
describe('Creates a schema that sets a default value if the input value is not specified', () => {
|
||||
type TestType = t.TypeOf<typeof TestType>;
|
||||
const TestType = t.union([t.string, t.number, t.null, t.undefined], 'TestType');
|
||||
|
||||
const DefaultValue = defaultValue(TestType, 42);
|
||||
|
||||
describe('Name of the schema', () => {
|
||||
it('has a default value', () => {
|
||||
expect(defaultValue(TestType, 42).name).toEqual('DefaultValue<TestType>');
|
||||
});
|
||||
|
||||
it('can be overriden', () => {
|
||||
expect(defaultValue(TestType, 42, 'CustomName').name).toEqual('CustomName');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation succeeds', () => {
|
||||
describe('when input is a valid value', () => {
|
||||
const cases = [
|
||||
{ input: 'foo' },
|
||||
{ input: '42' },
|
||||
{ input: 42 },
|
||||
// including all "falsey" values which are not null or undefined
|
||||
{ input: '' },
|
||||
{ input: 0 },
|
||||
];
|
||||
|
||||
cases.forEach(({ input }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = DefaultValue.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
const expectedOutput = input;
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation fails', () => {
|
||||
describe('when input is an invalid value', () => {
|
||||
const cases = [
|
||||
{
|
||||
input: {},
|
||||
expectedErrors: ['Invalid value "{}" supplied to "DefaultValue<TestType>"'],
|
||||
},
|
||||
{
|
||||
input: { foo: 42 },
|
||||
expectedErrors: ['Invalid value "{"foo":42}" supplied to "DefaultValue<TestType>"'],
|
||||
},
|
||||
{
|
||||
input: [],
|
||||
expectedErrors: ['Invalid value "[]" supplied to "DefaultValue<TestType>"'],
|
||||
},
|
||||
{
|
||||
input: ['foo', 42],
|
||||
expectedErrors: ['Invalid value "["foo",42]" supplied to "DefaultValue<TestType>"'],
|
||||
},
|
||||
];
|
||||
|
||||
cases.forEach(({ input, expectedErrors }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = DefaultValue.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual(expectedErrors);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation returns specified default value', () => {
|
||||
describe('when input is', () => {
|
||||
const cases = [{ input: null }, { input: undefined }];
|
||||
|
||||
cases.forEach(({ input }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = DefaultValue.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
const expectedOutput = 42;
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import type { Either } from 'fp-ts/lib/Either';
|
||||
|
||||
/**
|
||||
* Creates a schema that sets a default value if the input value is not specified.
|
||||
*
|
||||
* @param valueSchema Base schema of a value.
|
||||
* @param value Default value to set.
|
||||
* @param name (Optional) Name of the resulting schema.
|
||||
*/
|
||||
export const defaultValue = <TValue>(
|
||||
valueSchema: t.Type<TValue>,
|
||||
value: TValue,
|
||||
name?: string
|
||||
): t.Type<TValue> => {
|
||||
return new t.Type<TValue>(
|
||||
name ?? `DefaultValue<${valueSchema.name}>`,
|
||||
valueSchema.is,
|
||||
(input, context): Either<t.Errors, TValue> =>
|
||||
input == null ? t.success(value) : valueSchema.validate(input, context),
|
||||
t.identity
|
||||
);
|
||||
};
|
|
@ -9,10 +9,12 @@
|
|||
export * from './default_array';
|
||||
export * from './default_boolean_false';
|
||||
export * from './default_boolean_true';
|
||||
export * from './default_csv_array';
|
||||
export * from './default_empty_string';
|
||||
export * from './default_string_array';
|
||||
export * from './default_string_boolean_false';
|
||||
export * from './default_uuid';
|
||||
export * from './default_value';
|
||||
export * from './default_version_number';
|
||||
export * from './empty_string_array';
|
||||
export * from './enumeration';
|
||||
|
|
|
@ -32,6 +32,7 @@ const optionalDateFieldSchema = schema.maybe(
|
|||
const sortSchema = schema.object({
|
||||
sort_field: schema.oneOf([
|
||||
schema.literal('@timestamp'),
|
||||
schema.literal('event.sequence'), // can be used as a tiebreaker for @timestamp
|
||||
schema.literal('event.start'),
|
||||
schema.literal('event.end'),
|
||||
schema.literal('event.provider'),
|
||||
|
|
|
@ -231,6 +231,14 @@ export const IP_REPUTATION_LINKS_SETTING_DEFAULT = `[
|
|||
export const SHOW_RELATED_INTEGRATIONS_SETTING =
|
||||
'securitySolution:showRelatedIntegrations' as const;
|
||||
|
||||
/** This Kibana Advanced Setting enables extended rule execution logging to Event Log */
|
||||
export const EXTENDED_RULE_EXECUTION_LOGGING_ENABLED_SETTING =
|
||||
'securitySolution:extendedRuleExecutionLoggingEnabled' as const;
|
||||
|
||||
/** This Kibana Advanced Setting sets minimum log level starting from which execution logs will be written to Event Log */
|
||||
export const EXTENDED_RULE_EXECUTION_LOGGING_MIN_LEVEL_SETTING =
|
||||
'securitySolution:extendedRuleExecutionLoggingMinLevel' as const;
|
||||
|
||||
/**
|
||||
* Id for the notifications alerting type
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
|
@ -268,10 +276,6 @@ export const DETECTION_ENGINE_RULES_BULK_UPDATE =
|
|||
* Internal detection engine routes
|
||||
*/
|
||||
export const INTERNAL_DETECTION_ENGINE_URL = '/internal/detection_engine' as const;
|
||||
export const DETECTION_ENGINE_RULE_EXECUTION_EVENTS_URL =
|
||||
`${INTERNAL_DETECTION_ENGINE_URL}/rules/{ruleId}/execution/events` as const;
|
||||
export const detectionEngineRuleExecutionEventsUrl = (ruleId: string) =>
|
||||
`${INTERNAL_DETECTION_ENGINE_URL}/rules/${ruleId}/execution/events` as const;
|
||||
export const DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL =
|
||||
`${INTERNAL_DETECTION_ENGINE_URL}/fleet/integrations/installed` as const;
|
||||
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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 { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
|
||||
|
||||
import {
|
||||
GetRuleExecutionEventsRequestParams,
|
||||
GetRuleExecutionEventsRequestQuery,
|
||||
} from './request_schema';
|
||||
|
||||
describe('Request schema of Get rule execution events', () => {
|
||||
describe('GetRuleExecutionEventsRequestParams', () => {
|
||||
describe('Validation succeeds', () => {
|
||||
it('when required parameters are passed', () => {
|
||||
const input = {
|
||||
ruleId: 'some id',
|
||||
};
|
||||
|
||||
const decoded = GetRuleExecutionEventsRequestParams.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(
|
||||
expect.objectContaining({
|
||||
ruleId: 'some id',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('when unknown parameters are passed as well', () => {
|
||||
const input = {
|
||||
ruleId: 'some id',
|
||||
foo: 'bar', // this one is not in the schema and will be stripped
|
||||
};
|
||||
|
||||
const decoded = GetRuleExecutionEventsRequestParams.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual({
|
||||
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({});
|
||||
};
|
||||
|
||||
it('when not all the required parameters are passed', () => {
|
||||
const input = {};
|
||||
test(input);
|
||||
});
|
||||
|
||||
it('when ruleId is an empty string', () => {
|
||||
const input: GetRuleExecutionEventsRequestParams = {
|
||||
ruleId: '',
|
||||
};
|
||||
|
||||
test(input);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetRuleExecutionEventsRequestQuery', () => {
|
||||
describe('Validation succeeds', () => {
|
||||
it('when valid parameters are passed', () => {
|
||||
const input = {
|
||||
event_types: 'message,status-change',
|
||||
log_levels: 'debug,info,error',
|
||||
sort_order: 'asc',
|
||||
page: 42,
|
||||
per_page: 6,
|
||||
};
|
||||
|
||||
const decoded = GetRuleExecutionEventsRequestQuery.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual({
|
||||
event_types: ['message', 'status-change'],
|
||||
log_levels: ['debug', 'info', 'error'],
|
||||
sort_order: 'asc',
|
||||
page: 42,
|
||||
per_page: 6,
|
||||
});
|
||||
});
|
||||
|
||||
it('when unknown parameters are passed as well', () => {
|
||||
const input = {
|
||||
event_types: 'message,status-change',
|
||||
log_levels: 'debug,info,error',
|
||||
sort_order: 'asc',
|
||||
page: 42,
|
||||
per_page: 6,
|
||||
foo: 'bar', // this one is not in the schema and will be stripped
|
||||
};
|
||||
|
||||
const decoded = GetRuleExecutionEventsRequestQuery.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual({
|
||||
event_types: ['message', 'status-change'],
|
||||
log_levels: ['debug', 'info', 'error'],
|
||||
sort_order: 'asc',
|
||||
page: 42,
|
||||
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({});
|
||||
};
|
||||
|
||||
it('when invalid parameters are passed', () => {
|
||||
test({
|
||||
event_types: 'foo,status-change',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation sets default values', () => {
|
||||
it('when optional parameters are not passed', () => {
|
||||
const input = {};
|
||||
|
||||
const decoded = GetRuleExecutionEventsRequestQuery.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual({
|
||||
event_types: [],
|
||||
log_levels: [],
|
||||
sort_order: 'desc',
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 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, NonEmptyString } from '@kbn/securitysolution-io-ts-types';
|
||||
|
||||
import { DefaultSortOrderDesc } from '../../../schemas/common';
|
||||
import { TRuleExecutionEventType } from '../../model/execution_event';
|
||||
import { TLogLevel } from '../../model/log_level';
|
||||
|
||||
/**
|
||||
* 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.type({
|
||||
event_types: defaultCsvArray(TRuleExecutionEventType),
|
||||
log_levels: defaultCsvArray(TLogLevel),
|
||||
sort_order: DefaultSortOrderDesc, // defaults to 'desc'
|
||||
page: DefaultPage, // defaults to 1
|
||||
per_page: DefaultPerPage, // defaults to 20
|
||||
})
|
||||
);
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ruleExecutionEventMock } from '../../model/execution_event.mock';
|
||||
import type { GetRuleExecutionEventsResponse } from './response_schema';
|
||||
|
||||
const getSomeResponse = (): GetRuleExecutionEventsResponse => {
|
||||
const events = ruleExecutionEventMock.getSomeEvents();
|
||||
return {
|
||||
events,
|
||||
pagination: {
|
||||
page: 1,
|
||||
per_page: events.length,
|
||||
total: events.length * 10,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getRuleExecutionEventsResponseMock = {
|
||||
getSomeResponse,
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { PaginationResult } from '../../../schemas/common';
|
||||
import { RuleExecutionEvent } from '../../model/execution_event';
|
||||
|
||||
/**
|
||||
* 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,
|
||||
})
|
||||
);
|
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
* 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 { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
|
||||
|
||||
import { RULE_EXECUTION_STATUSES } from '../../model/execution_status';
|
||||
import { DefaultSortField, DefaultRuleExecutionStatusCsvArray } from './request_schema';
|
||||
|
||||
describe('Request schema of Get rule execution results', () => {
|
||||
describe('DefaultRuleExecutionStatusCsvArray', () => {
|
||||
describe('Validation succeeds', () => {
|
||||
describe('when input is a single rule execution status', () => {
|
||||
const cases = RULE_EXECUTION_STATUSES.map((supportedStatus) => {
|
||||
return { input: supportedStatus };
|
||||
});
|
||||
|
||||
cases.forEach(({ input }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = DefaultRuleExecutionStatusCsvArray.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
const expectedOutput = [input]; // note that it's an array after decode
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when input is an array of rule execution statuses', () => {
|
||||
const cases = [
|
||||
{ input: ['succeeded', 'failed'] },
|
||||
{ input: ['partial failure', 'going to run', 'running'] },
|
||||
];
|
||||
|
||||
cases.forEach(({ input }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = DefaultRuleExecutionStatusCsvArray.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
const expectedOutput = input;
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when input is a string which is a comma-separated array of statuses', () => {
|
||||
const cases = [
|
||||
{
|
||||
input: 'succeeded,failed',
|
||||
expectedOutput: ['succeeded', 'failed'],
|
||||
},
|
||||
{
|
||||
input: 'partial failure,going to run,running',
|
||||
expectedOutput: ['partial failure', 'going to run', 'running'],
|
||||
},
|
||||
];
|
||||
|
||||
cases.forEach(({ input, expectedOutput }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = DefaultRuleExecutionStatusCsvArray.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation fails', () => {
|
||||
describe('when input is a single invalid value', () => {
|
||||
const cases = [
|
||||
{
|
||||
input: 'val',
|
||||
expectedErrors: [
|
||||
'Invalid value "val" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
],
|
||||
},
|
||||
{
|
||||
input: '5',
|
||||
expectedErrors: [
|
||||
'Invalid value "5" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
],
|
||||
},
|
||||
{
|
||||
input: 5,
|
||||
expectedErrors: [
|
||||
'Invalid value "5" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
],
|
||||
},
|
||||
{
|
||||
input: {},
|
||||
expectedErrors: [
|
||||
'Invalid value "{}" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
cases.forEach(({ input, expectedErrors }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = DefaultRuleExecutionStatusCsvArray.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual(expectedErrors);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when input is an array of invalid values', () => {
|
||||
const cases = [
|
||||
{
|
||||
input: ['value 1', 5],
|
||||
expectedErrors: [
|
||||
'Invalid value "value 1" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
'Invalid value "5" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
],
|
||||
},
|
||||
{
|
||||
input: ['value 1', 'succeeded'],
|
||||
expectedErrors: [
|
||||
'Invalid value "value 1" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
],
|
||||
},
|
||||
{
|
||||
input: ['', 5, {}],
|
||||
expectedErrors: [
|
||||
'Invalid value "" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
'Invalid value "5" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
'Invalid value "{}" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
cases.forEach(({ input, expectedErrors }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = DefaultRuleExecutionStatusCsvArray.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual(expectedErrors);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when input is a string which is a comma-separated array of invalid values', () => {
|
||||
const cases = [
|
||||
{
|
||||
input: 'value 1,5',
|
||||
expectedErrors: [
|
||||
'Invalid value "value 1" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
'Invalid value "5" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
],
|
||||
},
|
||||
{
|
||||
input: 'value 1,succeeded',
|
||||
expectedErrors: [
|
||||
'Invalid value "value 1" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
],
|
||||
},
|
||||
{
|
||||
input: ',5,{}',
|
||||
expectedErrors: [
|
||||
'Invalid value "" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
'Invalid value "5" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
'Invalid value "{}" supplied to "DefaultCsvArray<RuleExecutionStatus>"',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
cases.forEach(({ input, expectedErrors }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = DefaultRuleExecutionStatusCsvArray.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual(expectedErrors);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation returns default value (an empty array)', () => {
|
||||
describe('when input is', () => {
|
||||
const cases = [{ input: null }, { input: undefined }, { input: '' }, { input: [] }];
|
||||
|
||||
cases.forEach(({ input }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = DefaultRuleExecutionStatusCsvArray.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
const expectedOutput: string[] = [];
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DefaultSortField', () => {
|
||||
describe('Validation succeeds', () => {
|
||||
describe('when input is a valid sort field', () => {
|
||||
const cases = [
|
||||
{ input: 'timestamp' },
|
||||
{ input: 'duration_ms' },
|
||||
{ input: 'gap_duration_s' },
|
||||
{ input: 'indexing_duration_ms' },
|
||||
{ input: 'search_duration_ms' },
|
||||
{ input: 'schedule_delay_ms' },
|
||||
];
|
||||
|
||||
cases.forEach(({ input }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = DefaultSortField.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(input);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation fails', () => {
|
||||
describe('when input is an invalid sort field', () => {
|
||||
const cases = [
|
||||
{ input: 'status' },
|
||||
{ input: 'message' },
|
||||
{ input: 'es_search_duration_ms' },
|
||||
{ input: 'security_status' },
|
||||
{ input: 'security_message' },
|
||||
];
|
||||
|
||||
cases.forEach(({ input }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = DefaultSortField.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
const expectedErrors = [`Invalid value "${input}" supplied to "DefaultSortField"`];
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual(expectedErrors);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation returns the default sort field "timestamp"', () => {
|
||||
describe('when input is', () => {
|
||||
const cases = [{ input: null }, { input: undefined }];
|
||||
|
||||
cases.forEach(({ input }) => {
|
||||
it(`${input}`, () => {
|
||||
const decoded = DefaultSortField.decode(input);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual('timestamp');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 { DefaultPage, DefaultPerPage } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import {
|
||||
defaultCsvArray,
|
||||
DefaultEmptyString,
|
||||
defaultValue,
|
||||
IsoDateString,
|
||||
NonEmptyString,
|
||||
} from '@kbn/securitysolution-io-ts-types';
|
||||
|
||||
import { DefaultSortOrderDesc } from '../../../schemas/common';
|
||||
import { SortFieldOfRuleExecutionResult } from '../../model/execution_result';
|
||||
import { TRuleExecutionStatus } from '../../model/execution_status';
|
||||
|
||||
/**
|
||||
* Types the DefaultRuleExecutionStatusCsvArray as:
|
||||
* - If not specified, then a default empty array will be set
|
||||
* - If an array is sent in, then the array will be validated to ensure all elements are a RuleExecutionStatus
|
||||
* (or that the array is empty)
|
||||
* - If a CSV string is sent in, then it will be parsed to an array which will be validated
|
||||
*/
|
||||
export const DefaultRuleExecutionStatusCsvArray = defaultCsvArray(TRuleExecutionStatus);
|
||||
|
||||
/**
|
||||
* Types the DefaultSortField as:
|
||||
* - If undefined, then a default sort field of 'timestamp' will be set
|
||||
* - If a string is sent in, then the string will be validated to ensure it is as valid sortFields
|
||||
*/
|
||||
export const DefaultSortField = defaultValue(
|
||||
SortFieldOfRuleExecutionResult,
|
||||
'timestamp',
|
||||
'DefaultSortField'
|
||||
);
|
||||
|
||||
/**
|
||||
* Path parameters of the API route.
|
||||
*/
|
||||
export type GetRuleExecutionResultsRequestParams = t.TypeOf<
|
||||
typeof GetRuleExecutionResultsRequestParams
|
||||
>;
|
||||
export const GetRuleExecutionResultsRequestParams = t.exact(
|
||||
t.type({
|
||||
ruleId: NonEmptyString,
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Query string parameters of the API route.
|
||||
*/
|
||||
export type GetRuleExecutionResultsRequestQuery = t.TypeOf<
|
||||
typeof GetRuleExecutionResultsRequestQuery
|
||||
>;
|
||||
export const GetRuleExecutionResultsRequestQuery = t.exact(
|
||||
t.type({
|
||||
start: IsoDateString,
|
||||
end: IsoDateString,
|
||||
query_text: DefaultEmptyString, // defaults to ''
|
||||
status_filters: DefaultRuleExecutionStatusCsvArray, // defaults to []
|
||||
sort_field: DefaultSortField, // defaults to 'timestamp'
|
||||
sort_order: DefaultSortOrderDesc, // defaults to 'desc'
|
||||
page: DefaultPage, // defaults to 1
|
||||
per_page: DefaultPerPage, // defaults to 20
|
||||
})
|
||||
);
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ruleExecutionResultMock } from '../../model/execution_result.mock';
|
||||
import type { GetRuleExecutionResultsResponse } from './response_schema';
|
||||
|
||||
const getSomeResponse = (): GetRuleExecutionResultsResponse => {
|
||||
const results = ruleExecutionResultMock.getSomeResults();
|
||||
return {
|
||||
events: results,
|
||||
total: results.length,
|
||||
};
|
||||
};
|
||||
|
||||
export const getRuleExecutionResultsResponseMock = {
|
||||
getSomeResponse,
|
||||
};
|
|
@ -6,15 +6,15 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { aggregateRuleExecutionEvent } from '../common';
|
||||
import { RuleExecutionResult } from '../../model/execution_result';
|
||||
|
||||
export const GetAggregateRuleExecutionEventsResponse = t.exact(
|
||||
/**
|
||||
* Response body of the API route.
|
||||
*/
|
||||
export type GetRuleExecutionResultsResponse = t.TypeOf<typeof GetRuleExecutionResultsResponse>;
|
||||
export const GetRuleExecutionResultsResponse = t.exact(
|
||||
t.type({
|
||||
events: t.array(aggregateRuleExecutionEvent),
|
||||
events: t.array(RuleExecutionResult),
|
||||
total: t.number,
|
||||
})
|
||||
);
|
||||
|
||||
export type GetAggregateRuleExecutionEventsResponse = t.TypeOf<
|
||||
typeof GetAggregateRuleExecutionEventsResponse
|
||||
>;
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { INTERNAL_DETECTION_ENGINE_URL as INTERNAL_URL } from '../../../constants';
|
||||
|
||||
export const GET_RULE_EXECUTION_EVENTS_URL =
|
||||
`${INTERNAL_URL}/rules/{ruleId}/execution/events` as const;
|
||||
export const getRuleExecutionEventsUrl = (ruleId: string) =>
|
||||
`${INTERNAL_URL}/rules/${ruleId}/execution/events` as const;
|
||||
|
||||
export const GET_RULE_EXECUTION_RESULTS_URL =
|
||||
`${INTERNAL_URL}/rules/{ruleId}/execution/results` as const;
|
||||
export const getRuleExecutionResultsUrl = (ruleId: string) =>
|
||||
`${INTERNAL_URL}/rules/${ruleId}/execution/results` as const;
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './api/get_rule_execution_events/request_schema';
|
||||
export * from './api/get_rule_execution_events/response_schema';
|
||||
export * from './api/get_rule_execution_results/request_schema';
|
||||
export * from './api/get_rule_execution_results/response_schema';
|
||||
export * from './api/urls';
|
||||
|
||||
export * from './model/execution_event';
|
||||
export * from './model/execution_metrics';
|
||||
export * from './model/execution_result';
|
||||
export * from './model/execution_settings';
|
||||
export * from './model/execution_status';
|
||||
export * from './model/execution_summary';
|
||||
export * from './model/log_level';
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './api/get_rule_execution_events/response_schema.mock';
|
||||
export * from './api/get_rule_execution_results/response_schema.mock';
|
||||
|
||||
export * from './model/execution_event.mock';
|
||||
export * from './model/execution_result.mock';
|
||||
export * from './model/execution_summary.mock';
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* 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 { RuleExecutionEvent } from './execution_event';
|
||||
import { RuleExecutionEventType } from './execution_event';
|
||||
import { LogLevel } from './log_level';
|
||||
|
||||
const DEFAULT_TIMESTAMP = '2021-12-28T10:10:00.806Z';
|
||||
const DEFAULT_SEQUENCE_NUMBER = 0;
|
||||
|
||||
const getMessageEvent = (props: Partial<RuleExecutionEvent> = {}): RuleExecutionEvent => {
|
||||
return {
|
||||
// Default values
|
||||
timestamp: DEFAULT_TIMESTAMP,
|
||||
sequence: DEFAULT_SEQUENCE_NUMBER,
|
||||
level: LogLevel.debug,
|
||||
message: 'Some message',
|
||||
// Overriden values
|
||||
...props,
|
||||
// Mandatory values for this type of event
|
||||
type: RuleExecutionEventType.message,
|
||||
};
|
||||
};
|
||||
|
||||
const getRunningStatusChange = (props: Partial<RuleExecutionEvent> = {}): RuleExecutionEvent => {
|
||||
return {
|
||||
// Default values
|
||||
timestamp: DEFAULT_TIMESTAMP,
|
||||
sequence: DEFAULT_SEQUENCE_NUMBER,
|
||||
message: 'Rule changed status to "running"',
|
||||
// Overriden values
|
||||
...props,
|
||||
// Mandatory values for this type of event
|
||||
level: LogLevel.info,
|
||||
type: RuleExecutionEventType['status-change'],
|
||||
};
|
||||
};
|
||||
|
||||
const getPartialFailureStatusChange = (
|
||||
props: Partial<RuleExecutionEvent> = {}
|
||||
): RuleExecutionEvent => {
|
||||
return {
|
||||
// Default values
|
||||
timestamp: DEFAULT_TIMESTAMP,
|
||||
sequence: DEFAULT_SEQUENCE_NUMBER,
|
||||
message: 'Rule changed status to "partial failure". Unknown error',
|
||||
// Overriden values
|
||||
...props,
|
||||
// Mandatory values for this type of event
|
||||
level: LogLevel.warn,
|
||||
type: RuleExecutionEventType['status-change'],
|
||||
};
|
||||
};
|
||||
|
||||
const getFailedStatusChange = (props: Partial<RuleExecutionEvent> = {}): RuleExecutionEvent => {
|
||||
return {
|
||||
// Default values
|
||||
timestamp: DEFAULT_TIMESTAMP,
|
||||
sequence: DEFAULT_SEQUENCE_NUMBER,
|
||||
message: 'Rule changed status to "failed". Unknown error',
|
||||
// Overriden values
|
||||
...props,
|
||||
// Mandatory values for this type of event
|
||||
level: LogLevel.error,
|
||||
type: RuleExecutionEventType['status-change'],
|
||||
};
|
||||
};
|
||||
|
||||
const getSucceededStatusChange = (props: Partial<RuleExecutionEvent> = {}): RuleExecutionEvent => {
|
||||
return {
|
||||
// Default values
|
||||
timestamp: DEFAULT_TIMESTAMP,
|
||||
sequence: DEFAULT_SEQUENCE_NUMBER,
|
||||
message: 'Rule changed status to "succeeded". Rule executed successfully',
|
||||
// Overriden values
|
||||
...props,
|
||||
// Mandatory values for this type of event
|
||||
level: LogLevel.info,
|
||||
type: RuleExecutionEventType['status-change'],
|
||||
};
|
||||
};
|
||||
|
||||
const getExecutionMetricsEvent = (props: Partial<RuleExecutionEvent> = {}): RuleExecutionEvent => {
|
||||
return {
|
||||
// Default values
|
||||
timestamp: DEFAULT_TIMESTAMP,
|
||||
sequence: DEFAULT_SEQUENCE_NUMBER,
|
||||
message: '',
|
||||
// Overriden values
|
||||
...props,
|
||||
// Mandatory values for this type of event
|
||||
level: LogLevel.debug,
|
||||
type: RuleExecutionEventType['execution-metrics'],
|
||||
};
|
||||
};
|
||||
|
||||
const getSomeEvents = (): RuleExecutionEvent[] => [
|
||||
getSucceededStatusChange({
|
||||
timestamp: '2021-12-28T10:10:09.806Z',
|
||||
sequence: 9,
|
||||
}),
|
||||
getExecutionMetricsEvent({
|
||||
timestamp: '2021-12-28T10:10:08.806Z',
|
||||
sequence: 8,
|
||||
}),
|
||||
getRunningStatusChange({
|
||||
timestamp: '2021-12-28T10:10:07.806Z',
|
||||
sequence: 7,
|
||||
}),
|
||||
getMessageEvent({
|
||||
timestamp: '2021-12-28T10:10:06.806Z',
|
||||
sequence: 6,
|
||||
level: LogLevel.debug,
|
||||
message: 'Rule execution started',
|
||||
}),
|
||||
getFailedStatusChange({
|
||||
timestamp: '2021-12-28T10:10:05.806Z',
|
||||
sequence: 5,
|
||||
}),
|
||||
getExecutionMetricsEvent({
|
||||
timestamp: '2021-12-28T10:10:04.806Z',
|
||||
sequence: 4,
|
||||
}),
|
||||
getPartialFailureStatusChange({
|
||||
timestamp: '2021-12-28T10:10:03.806Z',
|
||||
sequence: 3,
|
||||
}),
|
||||
getMessageEvent({
|
||||
timestamp: '2021-12-28T10:10:02.806Z',
|
||||
sequence: 2,
|
||||
level: LogLevel.error,
|
||||
message: 'Some error',
|
||||
}),
|
||||
getRunningStatusChange({
|
||||
timestamp: '2021-12-28T10:10:01.806Z',
|
||||
sequence: 1,
|
||||
}),
|
||||
getMessageEvent({
|
||||
timestamp: '2021-12-28T10:10:00.806Z',
|
||||
sequence: 0,
|
||||
level: LogLevel.debug,
|
||||
message: 'Rule execution started',
|
||||
}),
|
||||
];
|
||||
|
||||
export const ruleExecutionEventMock = {
|
||||
getSomeEvents,
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { enumeration, IsoDateString } 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,
|
||||
message: t.string,
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * 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>;
|
||||
export const RuleExecutionMetrics = t.partial({
|
||||
total_search_duration_ms: DurationMetric,
|
||||
total_indexing_duration_ms: DurationMetric,
|
||||
execution_gap_duration_s: DurationMetric,
|
||||
});
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* 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 { RuleExecutionResult } from './execution_result';
|
||||
|
||||
const getSomeResults = (): RuleExecutionResult[] => [
|
||||
{
|
||||
execution_uuid: 'dc45a63c-4872-4964-a2d0-bddd8b2e634d',
|
||||
timestamp: '2022-04-28T21:19:08.047Z',
|
||||
duration_ms: 3,
|
||||
status: 'failure',
|
||||
message: 'siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: execution failed',
|
||||
num_active_alerts: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 0,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 0,
|
||||
total_search_duration_ms: 0,
|
||||
es_search_duration_ms: 0,
|
||||
schedule_delay_ms: 2169,
|
||||
timed_out: false,
|
||||
indexing_duration_ms: 0,
|
||||
search_duration_ms: 0,
|
||||
gap_duration_s: 0,
|
||||
security_status: 'failed',
|
||||
security_message: 'Rule failed to execute because rule ran after it was disabled.',
|
||||
},
|
||||
{
|
||||
execution_uuid: '0fde9271-05d0-4bfb-8ff8-815756d28350',
|
||||
timestamp: '2022-04-28T21:19:04.973Z',
|
||||
duration_ms: 1446,
|
||||
status: 'success',
|
||||
message:
|
||||
"rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'",
|
||||
num_active_alerts: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 0,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 0,
|
||||
total_search_duration_ms: 0,
|
||||
es_search_duration_ms: 0,
|
||||
schedule_delay_ms: 2089,
|
||||
timed_out: false,
|
||||
indexing_duration_ms: 0,
|
||||
search_duration_ms: 2,
|
||||
gap_duration_s: 0,
|
||||
security_status: 'succeeded',
|
||||
security_message: 'succeeded',
|
||||
},
|
||||
{
|
||||
execution_uuid: '5daaa259-ded8-4a52-853e-1e7652d325d5',
|
||||
timestamp: '2022-04-28T21:19:01.976Z',
|
||||
duration_ms: 1395,
|
||||
status: 'success',
|
||||
message:
|
||||
"rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'",
|
||||
num_active_alerts: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 0,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 0,
|
||||
total_search_duration_ms: 0,
|
||||
es_search_duration_ms: 1,
|
||||
schedule_delay_ms: 2637,
|
||||
timed_out: false,
|
||||
indexing_duration_ms: 0,
|
||||
search_duration_ms: 3,
|
||||
gap_duration_s: 0,
|
||||
security_status: 'succeeded',
|
||||
security_message: 'succeeded',
|
||||
},
|
||||
{
|
||||
execution_uuid: 'c7223e1c-4264-4a27-8697-0d720243fafc',
|
||||
timestamp: '2022-04-28T21:18:58.431Z',
|
||||
duration_ms: 1815,
|
||||
status: 'success',
|
||||
message:
|
||||
"rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'",
|
||||
num_active_alerts: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 0,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 0,
|
||||
total_search_duration_ms: 0,
|
||||
es_search_duration_ms: 1,
|
||||
schedule_delay_ms: -255429,
|
||||
timed_out: false,
|
||||
indexing_duration_ms: 0,
|
||||
search_duration_ms: 3,
|
||||
gap_duration_s: 0,
|
||||
security_status: 'succeeded',
|
||||
security_message: 'succeeded',
|
||||
},
|
||||
{
|
||||
execution_uuid: '1f6ba0c1-cc36-4f45-b919-7790b8a8d670',
|
||||
timestamp: '2022-04-28T21:18:13.954Z',
|
||||
duration_ms: 2055,
|
||||
status: 'success',
|
||||
message:
|
||||
"rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'",
|
||||
num_active_alerts: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 0,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 0,
|
||||
total_search_duration_ms: 0,
|
||||
es_search_duration_ms: 0,
|
||||
schedule_delay_ms: 2027,
|
||||
timed_out: false,
|
||||
indexing_duration_ms: 0,
|
||||
search_duration_ms: 0,
|
||||
gap_duration_s: 0,
|
||||
security_status: 'partial failure',
|
||||
security_message:
|
||||
'Check privileges failed to execute ResponseError: index_not_found_exception: [index_not_found_exception] Reason: no such index [yup] name: "Click here for hot fresh alerts!" id: "a6e61cf0-c737-11ec-9e32-e14913ffdd2d" rule id: "34946b12-88d1-49ef-82b7-9cad45972030" execution id: "1f6ba0c1-cc36-4f45-b919-7790b8a8d670" space ID: "default"',
|
||||
},
|
||||
{
|
||||
execution_uuid: 'b0f65d64-b229-432b-9d39-f4385a7f9368',
|
||||
timestamp: '2022-04-28T21:15:43.086Z',
|
||||
duration_ms: 1205,
|
||||
status: 'success',
|
||||
message:
|
||||
"rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'",
|
||||
num_active_alerts: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 0,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 0,
|
||||
total_search_duration_ms: 0,
|
||||
es_search_duration_ms: 672,
|
||||
schedule_delay_ms: 3086,
|
||||
timed_out: false,
|
||||
indexing_duration_ms: 140,
|
||||
search_duration_ms: 684,
|
||||
gap_duration_s: 0,
|
||||
security_status: 'succeeded',
|
||||
security_message: 'succeeded',
|
||||
},
|
||||
{
|
||||
execution_uuid: '7bfd25b9-c0d8-44b1-982c-485169466a8e',
|
||||
timestamp: '2022-04-28T21:10:40.135Z',
|
||||
duration_ms: 6321,
|
||||
status: 'success',
|
||||
message:
|
||||
"rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'",
|
||||
num_active_alerts: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 0,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 0,
|
||||
total_search_duration_ms: 0,
|
||||
es_search_duration_ms: 930,
|
||||
schedule_delay_ms: 1222,
|
||||
timed_out: false,
|
||||
indexing_duration_ms: 2103,
|
||||
search_duration_ms: 946,
|
||||
gap_duration_s: 0,
|
||||
security_status: 'succeeded',
|
||||
security_message: 'succeeded',
|
||||
},
|
||||
];
|
||||
|
||||
export const ruleExecutionResultMock = {
|
||||
getSomeResults,
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { IsoDateString } from '@kbn/securitysolution-io-ts-types';
|
||||
|
||||
/**
|
||||
* Rule execution result is an aggregate that groups plain rule execution events by execution UUID.
|
||||
* It contains such information as execution UUID, date, status and metrics.
|
||||
*/
|
||||
export type RuleExecutionResult = t.TypeOf<typeof RuleExecutionResult>;
|
||||
export const RuleExecutionResult = t.type({
|
||||
execution_uuid: t.string,
|
||||
timestamp: IsoDateString,
|
||||
duration_ms: t.number,
|
||||
status: t.string,
|
||||
message: t.string,
|
||||
num_active_alerts: t.number,
|
||||
num_new_alerts: t.number,
|
||||
num_recovered_alerts: t.number,
|
||||
num_triggered_actions: t.number,
|
||||
num_succeeded_actions: t.number,
|
||||
num_errored_actions: t.number,
|
||||
total_search_duration_ms: t.number,
|
||||
es_search_duration_ms: t.number,
|
||||
schedule_delay_ms: t.number,
|
||||
timed_out: t.boolean,
|
||||
indexing_duration_ms: t.number,
|
||||
search_duration_ms: t.number,
|
||||
gap_duration_s: t.number,
|
||||
security_status: t.string,
|
||||
security_message: t.string,
|
||||
});
|
||||
|
||||
/**
|
||||
* We support sorting rule execution results by these fields.
|
||||
*/
|
||||
export type SortFieldOfRuleExecutionResult = t.TypeOf<typeof SortFieldOfRuleExecutionResult>;
|
||||
export const SortFieldOfRuleExecutionResult = t.keyof({
|
||||
timestamp: IsoDateString,
|
||||
duration_ms: t.number,
|
||||
gap_duration_s: t.number,
|
||||
indexing_duration_ms: t.number,
|
||||
search_duration_ms: t.number,
|
||||
schedule_delay_ms: t.number,
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface RuleExecutionSettings {
|
||||
extendedLogging: {
|
||||
isEnabled: boolean;
|
||||
minLevel: LogLevelSetting;
|
||||
};
|
||||
}
|
||||
|
||||
export enum LogLevelSetting {
|
||||
'trace' = 'trace',
|
||||
'debug' = 'debug',
|
||||
'info' = 'info',
|
||||
'warn' = 'warn',
|
||||
'error' = 'error',
|
||||
'off' = 'off',
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type * as t from 'io-ts';
|
||||
import { enumeration, PositiveInteger } from '@kbn/securitysolution-io-ts-types';
|
||||
import { assertUnreachable } from '../../../utility_types';
|
||||
|
||||
/**
|
||||
* Custom execution status of Security rules that is different from the status
|
||||
* used in the Alerting Framework. We merge our custom status with the
|
||||
* Framework's status to determine the resulting status of a rule.
|
||||
*/
|
||||
export enum RuleExecutionStatus {
|
||||
/**
|
||||
* @deprecated Replaced by the 'running' status but left for backwards compatibility
|
||||
* with rule execution events already written to Event Log in the prior versions of Kibana.
|
||||
* Don't use when writing rule status changes.
|
||||
*/
|
||||
'going to run' = 'going to run',
|
||||
|
||||
/**
|
||||
* Rule execution started but not reached any intermediate or final status.
|
||||
*/
|
||||
'running' = 'running',
|
||||
|
||||
/**
|
||||
* Rule can partially fail for various reasons either in the middle of an execution
|
||||
* (in this case we update its status right away) or in the end of it. So currently
|
||||
* this status can be both intermediate and final at the same time.
|
||||
* A typical reason for a partial failure: not all the indices that the rule searches
|
||||
* over actually exist.
|
||||
*/
|
||||
'partial failure' = 'partial failure',
|
||||
|
||||
/**
|
||||
* Rule failed to execute due to unhandled exception or a reason defined in the
|
||||
* business logic of its executor function.
|
||||
*/
|
||||
'failed' = 'failed',
|
||||
|
||||
/**
|
||||
* Rule executed successfully without any issues. Note: this status is just an indication
|
||||
* of a rule's "health". The rule might or might not generate any alerts despite of it.
|
||||
*/
|
||||
'succeeded' = 'succeeded',
|
||||
}
|
||||
|
||||
export const TRuleExecutionStatus = enumeration('RuleExecutionStatus', RuleExecutionStatus);
|
||||
|
||||
/**
|
||||
* An array of supported rule execution statuses.
|
||||
*/
|
||||
export const RULE_EXECUTION_STATUSES = Object.values(RuleExecutionStatus);
|
||||
|
||||
export type RuleExecutionStatusOrder = t.TypeOf<typeof RuleExecutionStatusOrder>;
|
||||
export const RuleExecutionStatusOrder = PositiveInteger;
|
||||
|
||||
export const ruleExecutionStatusToNumber = (
|
||||
status: RuleExecutionStatus
|
||||
): RuleExecutionStatusOrder => {
|
||||
switch (status) {
|
||||
case RuleExecutionStatus.succeeded:
|
||||
return 0;
|
||||
case RuleExecutionStatus['going to run']:
|
||||
return 10;
|
||||
case RuleExecutionStatus.running:
|
||||
return 15;
|
||||
case RuleExecutionStatus['partial failure']:
|
||||
return 20;
|
||||
case RuleExecutionStatus.failed:
|
||||
return 30;
|
||||
default:
|
||||
assertUnreachable(status);
|
||||
return 0;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RuleExecutionStatus } from './execution_status';
|
||||
import type { RuleExecutionSummary } from './execution_summary';
|
||||
|
||||
const getSummarySucceeded = (): RuleExecutionSummary => ({
|
||||
last_execution: {
|
||||
date: '2020-02-18T15:26:49.783Z',
|
||||
status: RuleExecutionStatus.succeeded,
|
||||
status_order: 0,
|
||||
message: 'succeeded',
|
||||
metrics: {
|
||||
total_search_duration_ms: 200,
|
||||
total_indexing_duration_ms: 800,
|
||||
execution_gap_duration_s: 500,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const getSummaryFailed = (): RuleExecutionSummary => ({
|
||||
last_execution: {
|
||||
date: '2020-02-18T15:15:58.806Z',
|
||||
status: RuleExecutionStatus.failed,
|
||||
status_order: 30,
|
||||
message:
|
||||
'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.',
|
||||
metrics: {
|
||||
total_search_duration_ms: 200,
|
||||
total_indexing_duration_ms: 800,
|
||||
execution_gap_duration_s: 500,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const ruleExecutionSummaryMock = {
|
||||
getSummarySucceeded,
|
||||
getSummaryFailed,
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { IsoDateString } from '@kbn/securitysolution-io-ts-types';
|
||||
import { TRuleExecutionStatus, RuleExecutionStatusOrder } from './execution_status';
|
||||
import { RuleExecutionMetrics } from './execution_metrics';
|
||||
|
||||
export type RuleExecutionSummary = t.TypeOf<typeof RuleExecutionSummary>;
|
||||
export const RuleExecutionSummary = t.type({
|
||||
last_execution: t.type({
|
||||
date: IsoDateString,
|
||||
status: TRuleExecutionStatus,
|
||||
status_order: RuleExecutionStatusOrder,
|
||||
message: t.string,
|
||||
metrics: RuleExecutionMetrics,
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { enumeration } from '@kbn/securitysolution-io-ts-types';
|
||||
import { enumFromString } from '../../../utils/enum_from_string';
|
||||
import { assertUnreachable } from '../../../utility_types';
|
||||
import { RuleExecutionStatus } from './execution_status';
|
||||
|
||||
export enum LogLevel {
|
||||
'trace' = 'trace',
|
||||
'debug' = 'debug',
|
||||
'info' = 'info',
|
||||
'warn' = 'warn',
|
||||
'error' = 'error',
|
||||
}
|
||||
|
||||
export const TLogLevel = enumeration('LogLevel', LogLevel);
|
||||
|
||||
/**
|
||||
* An array of supported log levels.
|
||||
*/
|
||||
export const LOG_LEVELS = Object.values(LogLevel);
|
||||
|
||||
export const logLevelToNumber = (level: keyof typeof LogLevel | null | undefined): number => {
|
||||
if (!level) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (level) {
|
||||
case 'trace':
|
||||
return 0;
|
||||
case 'debug':
|
||||
return 10;
|
||||
case 'info':
|
||||
return 20;
|
||||
case 'warn':
|
||||
return 30;
|
||||
case 'error':
|
||||
return 40;
|
||||
default:
|
||||
assertUnreachable(level);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const logLevelFromNumber = (num: number | null | undefined): LogLevel => {
|
||||
if (num === null || num === undefined || num < 10) {
|
||||
return LogLevel.trace;
|
||||
}
|
||||
if (num < 20) {
|
||||
return LogLevel.debug;
|
||||
}
|
||||
if (num < 30) {
|
||||
return LogLevel.info;
|
||||
}
|
||||
if (num < 40) {
|
||||
return LogLevel.warn;
|
||||
}
|
||||
return LogLevel.error;
|
||||
};
|
||||
|
||||
export const logLevelFromString = enumFromString(LogLevel);
|
||||
|
||||
export const logLevelFromExecutionStatus = (status: RuleExecutionStatus): LogLevel => {
|
||||
switch (status) {
|
||||
case RuleExecutionStatus['going to run']:
|
||||
case RuleExecutionStatus.running:
|
||||
case RuleExecutionStatus.succeeded:
|
||||
return LogLevel.info;
|
||||
case RuleExecutionStatus['partial failure']:
|
||||
return LogLevel.warn;
|
||||
case RuleExecutionStatus.failed:
|
||||
return LogLevel.error;
|
||||
default:
|
||||
assertUnreachable(status);
|
||||
return LogLevel.trace;
|
||||
}
|
||||
};
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
export * from './installed_integrations';
|
||||
export * from './rule_monitoring';
|
||||
export * from './pagination';
|
||||
export * from './rule_params';
|
||||
export * from './schemas';
|
||||
export * from './sorting';
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * 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,
|
||||
});
|
|
@ -1,147 +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, PositiveInteger } from '@kbn/securitysolution-io-ts-types';
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Rule execution status
|
||||
|
||||
/**
|
||||
* Custom execution status of Security rules that is different from the status
|
||||
* used in the Alerting Framework. We merge our custom status with the
|
||||
* Framework's status to determine the resulting status of a rule.
|
||||
*/
|
||||
export enum RuleExecutionStatus {
|
||||
/**
|
||||
* @deprecated Replaced by the 'running' status but left for backwards compatibility
|
||||
* with rule execution events already written to Event Log in the prior versions of Kibana.
|
||||
* Don't use when writing rule status changes.
|
||||
*/
|
||||
'going to run' = 'going to run',
|
||||
|
||||
/**
|
||||
* Rule execution started but not reached any intermediate or final status.
|
||||
*/
|
||||
'running' = 'running',
|
||||
|
||||
/**
|
||||
* Rule can partially fail for various reasons either in the middle of an execution
|
||||
* (in this case we update its status right away) or in the end of it. So currently
|
||||
* this status can be both intermediate and final at the same time.
|
||||
* A typical reason for a partial failure: not all the indices that the rule searches
|
||||
* over actually exist.
|
||||
*/
|
||||
'partial failure' = 'partial failure',
|
||||
|
||||
/**
|
||||
* Rule failed to execute due to unhandled exception or a reason defined in the
|
||||
* business logic of its executor function.
|
||||
*/
|
||||
'failed' = 'failed',
|
||||
|
||||
/**
|
||||
* Rule executed successfully without any issues. Note: this status is just an indication
|
||||
* of a rule's "health". The rule might or might not generate any alerts despite of it.
|
||||
*/
|
||||
'succeeded' = 'succeeded',
|
||||
}
|
||||
|
||||
export const ruleExecutionStatus = enumeration('RuleExecutionStatus', RuleExecutionStatus);
|
||||
|
||||
export const ruleExecutionStatusOrder = PositiveInteger;
|
||||
export type RuleExecutionStatusOrder = t.TypeOf<typeof ruleExecutionStatusOrder>;
|
||||
|
||||
export const ruleExecutionStatusOrderByStatus: Record<
|
||||
RuleExecutionStatus,
|
||||
RuleExecutionStatusOrder
|
||||
> = {
|
||||
[RuleExecutionStatus.succeeded]: 0,
|
||||
[RuleExecutionStatus['going to run']]: 10,
|
||||
[RuleExecutionStatus.running]: 15,
|
||||
[RuleExecutionStatus['partial failure']]: 20,
|
||||
[RuleExecutionStatus.failed]: 30,
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Rule execution metrics
|
||||
|
||||
export const durationMetric = PositiveInteger;
|
||||
export type DurationMetric = t.TypeOf<typeof durationMetric>;
|
||||
|
||||
export const ruleExecutionMetrics = t.partial({
|
||||
total_search_duration_ms: durationMetric,
|
||||
total_indexing_duration_ms: durationMetric,
|
||||
execution_gap_duration_s: durationMetric,
|
||||
});
|
||||
|
||||
export type RuleExecutionMetrics = t.TypeOf<typeof ruleExecutionMetrics>;
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Rule execution summary
|
||||
|
||||
export const ruleExecutionSummary = t.type({
|
||||
last_execution: t.type({
|
||||
date: IsoDateString,
|
||||
status: ruleExecutionStatus,
|
||||
status_order: ruleExecutionStatusOrder,
|
||||
message: t.string,
|
||||
metrics: ruleExecutionMetrics,
|
||||
}),
|
||||
});
|
||||
|
||||
export type RuleExecutionSummary = t.TypeOf<typeof ruleExecutionSummary>;
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Rule execution events
|
||||
|
||||
export const ruleExecutionEvent = t.type({
|
||||
date: IsoDateString,
|
||||
status: ruleExecutionStatus,
|
||||
message: t.string,
|
||||
});
|
||||
|
||||
export type RuleExecutionEvent = t.TypeOf<typeof ruleExecutionEvent>;
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Aggregate Rule execution events
|
||||
|
||||
export const aggregateRuleExecutionEvent = t.type({
|
||||
execution_uuid: t.string,
|
||||
timestamp: IsoDateString,
|
||||
duration_ms: t.number,
|
||||
status: t.string,
|
||||
message: t.string,
|
||||
num_active_alerts: t.number,
|
||||
num_new_alerts: t.number,
|
||||
num_recovered_alerts: t.number,
|
||||
num_triggered_actions: t.number,
|
||||
num_succeeded_actions: t.number,
|
||||
num_errored_actions: t.number,
|
||||
total_search_duration_ms: t.number,
|
||||
es_search_duration_ms: t.number,
|
||||
schedule_delay_ms: t.number,
|
||||
timed_out: t.boolean,
|
||||
indexing_duration_ms: t.number,
|
||||
search_duration_ms: t.number,
|
||||
gap_duration_s: t.number,
|
||||
security_status: t.string,
|
||||
security_message: t.string,
|
||||
});
|
||||
|
||||
export type AggregateRuleExecutionEvent = t.TypeOf<typeof aggregateRuleExecutionEvent>;
|
||||
|
||||
export const executionLogTableSortColumns = t.keyof({
|
||||
timestamp: IsoDateString,
|
||||
duration_ms: t.number,
|
||||
gap_duration_s: t.number,
|
||||
indexing_duration_ms: t.number,
|
||||
search_duration_ms: t.number,
|
||||
schedule_delay_ms: t.number,
|
||||
});
|
||||
|
||||
export type ExecutionLogTableSortColumns = t.TypeOf<typeof executionLogTableSortColumns>;
|
|
@ -193,36 +193,12 @@ export type QueryFilterOrUndefined = t.TypeOf<typeof queryFilterOrUndefined>;
|
|||
export const references = t.array(t.string);
|
||||
export type References = t.TypeOf<typeof references>;
|
||||
|
||||
export const per_page = PositiveInteger;
|
||||
export type PerPage = t.TypeOf<typeof per_page>;
|
||||
|
||||
export const perPageOrUndefined = t.union([per_page, t.undefined]);
|
||||
export type PerPageOrUndefined = t.TypeOf<typeof perPageOrUndefined>;
|
||||
|
||||
export const page = PositiveIntegerGreaterThanZero;
|
||||
export type Page = t.TypeOf<typeof page>;
|
||||
|
||||
export const pageOrUndefined = t.union([page, t.undefined]);
|
||||
export type PageOrUndefined = t.TypeOf<typeof pageOrUndefined>;
|
||||
|
||||
export const signal_ids = t.array(t.string);
|
||||
export type SignalIds = t.TypeOf<typeof signal_ids>;
|
||||
|
||||
// TODO: Can this be more strict or is this is the set of all Elastic Queries?
|
||||
export const signal_status_query = t.object;
|
||||
|
||||
export const sort_field = t.string;
|
||||
export type SortField = t.TypeOf<typeof sort_field>;
|
||||
|
||||
export const sortFieldOrUndefined = t.union([sort_field, t.undefined]);
|
||||
export type SortFieldOrUndefined = t.TypeOf<typeof sortFieldOrUndefined>;
|
||||
|
||||
export const sort_order = t.keyof({ asc: null, desc: null });
|
||||
export type SortOrder = t.TypeOf<typeof sort_order>;
|
||||
|
||||
export const sortOrderOrUndefined = t.union([sort_order, t.undefined]);
|
||||
export type SortOrderOrUndefined = t.TypeOf<typeof sortOrderOrUndefined>;
|
||||
|
||||
export const tags = t.array(t.string);
|
||||
export type Tags = t.TypeOf<typeof tags>;
|
||||
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { DefaultSortOrderAsc, DefaultSortOrderDesc } from './sorting';
|
||||
|
||||
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('DefaultSortOrderDesc', () => {
|
||||
describe('Validation succeeds', () => {
|
||||
it('when valid sort order is passed', () => {
|
||||
const payload = 'asc';
|
||||
const decoded = DefaultSortOrderDesc.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 = 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 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 SortField = t.TypeOf<typeof SortField>;
|
||||
export const SortField = t.string;
|
||||
|
||||
export type SortFieldOrUndefined = t.TypeOf<typeof SortFieldOrUndefined>;
|
||||
export const SortFieldOrUndefined = t.union([SortField, t.undefined]);
|
||||
|
||||
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');
|
|
@ -8,22 +8,22 @@
|
|||
import * as t from 'io-ts';
|
||||
|
||||
import { DefaultPerPage, DefaultPage } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import type { PerPage, Page } from '../common/schemas';
|
||||
import { queryFilter, fields, sort_field, sort_order } from '../common/schemas';
|
||||
import type { PerPage, Page } from '../common';
|
||||
import { queryFilter, fields, SortField, SortOrder } from '../common';
|
||||
|
||||
export const findRulesSchema = t.exact(
|
||||
t.partial({
|
||||
fields,
|
||||
filter: queryFilter,
|
||||
per_page: DefaultPerPage, // defaults to "20" if not sent in during decode
|
||||
page: DefaultPage, // defaults to "1" if not sent in during decode
|
||||
sort_field,
|
||||
sort_order,
|
||||
sort_field: SortField,
|
||||
sort_order: SortOrder,
|
||||
page: DefaultPage, // defaults to 1
|
||||
per_page: DefaultPerPage, // defaults to 20
|
||||
})
|
||||
);
|
||||
|
||||
export type FindRulesSchema = t.TypeOf<typeof findRulesSchema>;
|
||||
export type FindRulesSchemaDecoded = Omit<FindRulesSchema, 'per_page'> & {
|
||||
per_page: PerPage;
|
||||
page: Page;
|
||||
per_page: PerPage;
|
||||
};
|
||||
|
|
|
@ -1,117 +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 { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
|
||||
import {
|
||||
DefaultSortField,
|
||||
DefaultSortOrder,
|
||||
DefaultStatusFiltersStringArray,
|
||||
} from './get_rule_execution_events_schema';
|
||||
|
||||
describe('get_rule_execution_events_schema', () => {
|
||||
describe('DefaultStatusFiltersStringArray', () => {
|
||||
test('it should validate a single ruleExecutionStatus', () => {
|
||||
const payload = 'succeeded';
|
||||
const decoded = DefaultStatusFiltersStringArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual([payload]);
|
||||
});
|
||||
test('it should validate an array of ruleExecutionStatus joined by "\'"', () => {
|
||||
const payload = ['succeeded', 'failed'];
|
||||
const decoded = DefaultStatusFiltersStringArray.decode(payload.join(','));
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should not validate an invalid ruleExecutionStatus', () => {
|
||||
const payload = ['value 1', 5].join(',');
|
||||
const decoded = DefaultStatusFiltersStringArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "value 1" supplied to "DefaultStatusFiltersStringArray"',
|
||||
'Invalid value "5" supplied to "DefaultStatusFiltersStringArray"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should return a default array entry', () => {
|
||||
const payload = null;
|
||||
const decoded = DefaultStatusFiltersStringArray.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual([]);
|
||||
});
|
||||
});
|
||||
describe('DefaultSortField', () => {
|
||||
test('it should validate a valid sort field', () => {
|
||||
const payload = 'duration_ms';
|
||||
const decoded = DefaultSortField.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should not validate an invalid sort field', () => {
|
||||
const payload = 'es_search_duration_ms';
|
||||
const decoded = DefaultSortField.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "es_search_duration_ms" supplied to "DefaultSortField"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should return the default sort field "timestamp"', () => {
|
||||
const payload = null;
|
||||
const decoded = DefaultSortField.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual('timestamp');
|
||||
});
|
||||
});
|
||||
describe('DefaultSortOrder', () => {
|
||||
test('it should validate a valid sort order', () => {
|
||||
const payload = 'asc';
|
||||
const decoded = DefaultSortOrder.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should not validate an invalid sort order', () => {
|
||||
const payload = 'behind_you';
|
||||
const decoded = DefaultSortOrder.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "behind_you" supplied to "DefaultSortOrder"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should return the default sort order "desc"', () => {
|
||||
const payload = null;
|
||||
const decoded = DefaultSortOrder.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual('desc');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,105 +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 { DefaultEmptyString, IsoDateString } from '@kbn/securitysolution-io-ts-types';
|
||||
|
||||
import type { Either } from 'fp-ts/lib/Either';
|
||||
import type { ExecutionLogTableSortColumns, RuleExecutionStatus } from '../common';
|
||||
import { executionLogTableSortColumns, ruleExecutionStatus } from '../common';
|
||||
|
||||
/**
|
||||
* Types the DefaultStatusFiltersStringArray as:
|
||||
* - If undefined, then a default array will be set
|
||||
* - If an array is sent in, then the array will be validated to ensure all elements are a ruleExecutionStatus (or that the array is empty)
|
||||
*/
|
||||
export const DefaultStatusFiltersStringArray = new t.Type<
|
||||
RuleExecutionStatus[],
|
||||
RuleExecutionStatus[],
|
||||
unknown
|
||||
>(
|
||||
'DefaultStatusFiltersStringArray',
|
||||
t.array(ruleExecutionStatus).is,
|
||||
(input, context): Either<t.Errors, RuleExecutionStatus[]> => {
|
||||
if (input == null) {
|
||||
return t.success([]);
|
||||
} else if (typeof input === 'string') {
|
||||
if (input === '') {
|
||||
return t.success([]);
|
||||
} else {
|
||||
return t.array(ruleExecutionStatus).validate(input.split(','), context);
|
||||
}
|
||||
} else {
|
||||
return t.array(ruleExecutionStatus).validate(input, context);
|
||||
}
|
||||
},
|
||||
t.identity
|
||||
);
|
||||
|
||||
/**
|
||||
* Types the DefaultSortField as:
|
||||
* - If undefined, then a default sort field of 'timestamp' will be set
|
||||
* - If a string is sent in, then the string will be validated to ensure it is as valid sortFields
|
||||
*/
|
||||
export const DefaultSortField = new t.Type<
|
||||
ExecutionLogTableSortColumns,
|
||||
ExecutionLogTableSortColumns,
|
||||
unknown
|
||||
>(
|
||||
'DefaultSortField',
|
||||
executionLogTableSortColumns.is,
|
||||
(input, context): Either<t.Errors, ExecutionLogTableSortColumns> =>
|
||||
input == null ? t.success('timestamp') : executionLogTableSortColumns.validate(input, context),
|
||||
t.identity
|
||||
);
|
||||
|
||||
const sortOrder = t.keyof({ asc: null, desc: null });
|
||||
type SortOrder = t.TypeOf<typeof sortOrder>;
|
||||
|
||||
/**
|
||||
* Types the DefaultSortOrder 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 is as valid sortOrder
|
||||
*/
|
||||
export const DefaultSortOrder = new t.Type<SortOrder, SortOrder, unknown>(
|
||||
'DefaultSortOrder',
|
||||
sortOrder.is,
|
||||
(input, context): Either<t.Errors, SortOrder> =>
|
||||
input == null ? t.success('desc') : sortOrder.validate(input, context),
|
||||
t.identity
|
||||
);
|
||||
|
||||
/**
|
||||
* Route Request Params
|
||||
*/
|
||||
export const GetRuleExecutionEventsRequestParams = t.exact(
|
||||
t.type({
|
||||
ruleId: t.string,
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Route Query Params (as constructed from the above codecs)
|
||||
*/
|
||||
export const GetRuleExecutionEventsQueryParams = t.exact(
|
||||
t.type({
|
||||
start: IsoDateString,
|
||||
end: IsoDateString,
|
||||
query_text: DefaultEmptyString, // default to "" if not sent in during decode
|
||||
status_filters: DefaultStatusFiltersStringArray, // defaults to empty array if not sent in during decode
|
||||
per_page: DefaultPerPage, // defaults to "20" if not sent in during decode
|
||||
page: DefaultPage, // defaults to "1" if not sent in during decode
|
||||
sort_field: DefaultSortField, // defaults to "desc" if not sent in during decode
|
||||
sort_order: DefaultSortOrder, // defaults to "timestamp" if not sent in during decode
|
||||
})
|
||||
);
|
||||
|
||||
export type GetRuleExecutionEventsRequestParams = t.TypeOf<
|
||||
typeof GetRuleExecutionEventsRequestParams
|
||||
>;
|
|
@ -12,9 +12,9 @@ export * from './find_rules_schema';
|
|||
export * from './import_rules_schema';
|
||||
export * from './patch_rules_bulk_schema';
|
||||
export * from './patch_rules_schema';
|
||||
export * from './perform_bulk_action_schema';
|
||||
export * from './query_rules_schema';
|
||||
export * from './query_signals_index_schema';
|
||||
export * from './rule_schemas';
|
||||
export * from './set_signal_status_schema';
|
||||
export * from './update_rules_bulk_schema';
|
||||
export * from './rule_schemas';
|
||||
export * from './perform_bulk_action_schema';
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
import { listArray } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { version } from '@kbn/securitysolution-io-ts-types';
|
||||
|
||||
import { RuleExecutionSummary } from '../../rule_monitoring';
|
||||
import {
|
||||
id,
|
||||
index,
|
||||
|
@ -70,7 +71,6 @@ import {
|
|||
created_at,
|
||||
created_by,
|
||||
namespace,
|
||||
ruleExecutionSummary,
|
||||
RelatedIntegrationArray,
|
||||
RequiredFieldArray,
|
||||
SetupGuide,
|
||||
|
@ -486,7 +486,7 @@ const responseRequiredFields = {
|
|||
};
|
||||
|
||||
const responseOptionalFields = {
|
||||
execution_summary: ruleExecutionSummary,
|
||||
execution_summary: RuleExecutionSummary,
|
||||
};
|
||||
|
||||
export const fullResponseSchema = t.intersection([
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
export * from './error_schema';
|
||||
export * from './get_installed_integrations_response_schema';
|
||||
export * from './get_rule_execution_events_response';
|
||||
export * from './import_rules_schema';
|
||||
export * from './prepackaged_rules_schema';
|
||||
export * from './prepackaged_rules_status_schema';
|
||||
|
|
|
@ -34,10 +34,11 @@ import {
|
|||
max_signals,
|
||||
} from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import { DefaultStringArray, version } from '@kbn/securitysolution-io-ts-types';
|
||||
|
||||
import { DefaultListArray } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { isMlRule } from '../../../machine_learning/helpers';
|
||||
import { isThresholdRule } from '../../utils';
|
||||
import { RuleExecutionSummary } from '../../rule_monitoring';
|
||||
import {
|
||||
anomaly_threshold,
|
||||
data_view_id,
|
||||
|
@ -77,7 +78,6 @@ import {
|
|||
rule_name_override,
|
||||
timestamp_override,
|
||||
namespace,
|
||||
ruleExecutionSummary,
|
||||
RelatedIntegrationArray,
|
||||
RequiredFieldArray,
|
||||
SetupGuide,
|
||||
|
@ -189,7 +189,7 @@ export const partialRulesSchema = t.partial({
|
|||
namespace,
|
||||
note,
|
||||
uuid: id, // Move to 'required' post-migration
|
||||
execution_summary: ruleExecutionSummary,
|
||||
execution_summary: RuleExecutionSummary,
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -37,6 +37,7 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
* Enables the Endpoint response actions console in various areas of the app
|
||||
*/
|
||||
responseActionsConsoleEnabled: true,
|
||||
|
||||
/**
|
||||
* Enables the cloud security posture navigation inside the security solution
|
||||
*/
|
||||
|
@ -46,6 +47,15 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
* Enables the insights module for related alerts by process ancestry
|
||||
*/
|
||||
insightsRelatedAlertsByProcessAncestry: false,
|
||||
|
||||
/**
|
||||
* Enables extended rule execution logging to Event Log. When this setting is enabled:
|
||||
* - Rules write their console error, info, debug, and trace messages to Event Log,
|
||||
* in addition to other events they log there (status changes and execution metrics).
|
||||
* - We add a Kibana Advanced Setting that controls this behavior (on/off and log level).
|
||||
* - We show a table with plain execution logs on the Rule Details page.
|
||||
*/
|
||||
extendedRuleExecutionLoggingEnabled: false,
|
||||
});
|
||||
|
||||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
|
|
|
@ -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 { enumFromString } from './enum_from_string';
|
||||
|
||||
describe('enumFromString', () => {
|
||||
enum TestStringEnum {
|
||||
'foo' = 'foo',
|
||||
'bar' = 'bar',
|
||||
}
|
||||
|
||||
const testEnumFromString = enumFromString(TestStringEnum);
|
||||
|
||||
it('returns enum if provided with a known value', () => {
|
||||
expect(testEnumFromString('foo')).toEqual(TestStringEnum.foo);
|
||||
expect(testEnumFromString('bar')).toEqual(TestStringEnum.bar);
|
||||
});
|
||||
|
||||
it('returns null if provided with an unknown value', () => {
|
||||
expect(testEnumFromString('xyz')).toEqual(null);
|
||||
expect(testEnumFromString('123')).toEqual(null);
|
||||
});
|
||||
|
||||
it('returns null if provided with null', () => {
|
||||
expect(testEnumFromString(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it('returns null if provided with undefined', () => {
|
||||
expect(testEnumFromString(undefined)).toEqual(null);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
interface Enum<T> {
|
||||
[s: string]: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: It works only with string enums.
|
||||
* https://www.typescriptlang.org/docs/handbook/enums.html#string-enums
|
||||
*
|
||||
* Converts a string into a corresponding enum value.
|
||||
* Returns null if the value is not in the enum.
|
||||
*
|
||||
* @param enm Specified enum.
|
||||
* @returns Enum value or null.
|
||||
*
|
||||
* @example
|
||||
* enum MyEnum {
|
||||
* 'foo' = 'foo',
|
||||
* 'bar' = 'bar',
|
||||
* }
|
||||
*
|
||||
* const foo = enumFromString(MyEnum)('foo'); // MyEnum.foo
|
||||
* const bar = enumFromString(MyEnum)('bar'); // MyEnum.bar
|
||||
* const unknown = enumFromString(MyEnum)('xyz'); // null
|
||||
*/
|
||||
export const enumFromString = <T>(enm: Enum<T>) => {
|
||||
const supportedEnumValues = Object.values(enm) as unknown as string[];
|
||||
return (value: string | null | undefined): T | null => {
|
||||
return value && supportedEnumValues.includes(value) ? (value as unknown as T) : null;
|
||||
};
|
||||
};
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const toArray = <T = string>(value: T | T[] | null): T[] =>
|
||||
export const toArray = <T = string>(value: T | T[] | null | undefined): T[] =>
|
||||
Array.isArray(value) ? value : value == null ? [] : [value];
|
||||
|
||||
export const toStringArray = <T = string>(value: T | T[] | null): string[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.reduce<string[]>((acc, v) => {
|
||||
|
@ -41,6 +42,7 @@ export const toStringArray = <T = string>(value: T | T[] | null): string[] => {
|
|||
return [`${value}`];
|
||||
}
|
||||
};
|
||||
|
||||
export const toObjectArrayOfStrings = <T = string>(
|
||||
value: T | T[] | null
|
||||
): Array<{
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { EuiHealthProps } from '@elastic/eui';
|
||||
import { EuiHealth, EuiToolTip } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StatusTextWrapper = styled.div`
|
||||
width: 100%;
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/x-pack/plugins/security_solution/public/detection_engine'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/detection_engine',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/security_solution/public/detection_engine/**/*.{ts,tsx}',
|
||||
],
|
||||
// See: https://github.com/elastic/kibana/issues/117255, the moduleNameMapper creates mocks to avoid memory leaks from kibana core.
|
||||
moduleNameMapper: {
|
||||
'core/server$': '<rootDir>/x-pack/plugins/security_solution/server/__mocks__/core.mock.ts',
|
||||
'task_manager/server$':
|
||||
'<rootDir>/x-pack/plugins/security_solution/server/__mocks__/task_manager.mock.ts',
|
||||
'alerting/server$': '<rootDir>/x-pack/plugins/security_solution/server/__mocks__/alert.mock.ts',
|
||||
'actions/server$': '<rootDir>/x-pack/plugins/security_solution/server/__mocks__/action.mock.ts',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 {
|
||||
GetRuleExecutionEventsResponse,
|
||||
GetRuleExecutionResultsResponse,
|
||||
} from '../../../../../common/detection_engine/rule_monitoring';
|
||||
import {
|
||||
LogLevel,
|
||||
RuleExecutionEventType,
|
||||
} from '../../../../../common/detection_engine/rule_monitoring';
|
||||
|
||||
import type {
|
||||
FetchRuleExecutionEventsArgs,
|
||||
FetchRuleExecutionResultsArgs,
|
||||
IRuleMonitoringApiClient,
|
||||
} from '../api_client_interface';
|
||||
|
||||
export const api: jest.Mocked<IRuleMonitoringApiClient> = {
|
||||
fetchRuleExecutionEvents: jest
|
||||
.fn<Promise<GetRuleExecutionEventsResponse>, [FetchRuleExecutionEventsArgs]>()
|
||||
.mockResolvedValue({
|
||||
events: [
|
||||
{
|
||||
timestamp: '2021-12-29T10:42:59.996Z',
|
||||
sequence: 0,
|
||||
level: LogLevel.info,
|
||||
type: RuleExecutionEventType['status-change'],
|
||||
message: 'Rule changed status to "succeeded". Rule execution completed without errors',
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 1,
|
||||
},
|
||||
}),
|
||||
|
||||
fetchRuleExecutionResults: jest
|
||||
.fn<Promise<GetRuleExecutionResultsResponse>, [FetchRuleExecutionResultsArgs]>()
|
||||
.mockResolvedValue({
|
||||
events: [
|
||||
{
|
||||
duration_ms: 3866,
|
||||
es_search_duration_ms: 1236,
|
||||
execution_uuid: '88d15095-7937-462c-8f21-9763e1387cad',
|
||||
gap_duration_s: 0,
|
||||
indexing_duration_ms: 95,
|
||||
message:
|
||||
"rule executed: siem.queryRule:fb1fc150-a292-11ec-a2cf-c1b28b0392b0: 'Lots of Execution Events'",
|
||||
num_active_alerts: 0,
|
||||
num_errored_actions: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_succeeded_actions: 1,
|
||||
num_triggered_actions: 1,
|
||||
schedule_delay_ms: -127535,
|
||||
search_duration_ms: 1255,
|
||||
security_message: 'succeeded',
|
||||
security_status: 'succeeded',
|
||||
status: 'success',
|
||||
timed_out: false,
|
||||
timestamp: '2022-03-13T06:04:05.838Z',
|
||||
total_search_duration_ms: 0,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
}),
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './api_client';
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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 { KibanaServices } from '../../../common/lib/kibana';
|
||||
|
||||
import type {
|
||||
GetRuleExecutionEventsResponse,
|
||||
GetRuleExecutionResultsResponse,
|
||||
} from '../../../../common/detection_engine/rule_monitoring';
|
||||
import {
|
||||
LogLevel,
|
||||
RuleExecutionEventType,
|
||||
} from '../../../../common/detection_engine/rule_monitoring';
|
||||
|
||||
import { api } from './api_client';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
describe('Rule Monitoring API Client', () => {
|
||||
const fetchMock = jest.fn();
|
||||
const mockKibanaServices = KibanaServices.get as jest.Mock;
|
||||
mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } });
|
||||
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
describe('fetchRuleExecutionEvents', () => {
|
||||
const responseMock: GetRuleExecutionEventsResponse = {
|
||||
events: [],
|
||||
pagination: {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue(responseMock);
|
||||
});
|
||||
|
||||
it('calls API correctly with only rule id specified', async () => {
|
||||
await api.fetchRuleExecutionEvents({ ruleId: '42', signal });
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/internal/detection_engine/rules/42/execution/events',
|
||||
{
|
||||
method: 'GET',
|
||||
query: {},
|
||||
signal,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('calls API correctly with filter and pagination options', async () => {
|
||||
await api.fetchRuleExecutionEvents({
|
||||
ruleId: '42',
|
||||
eventTypes: [RuleExecutionEventType.message],
|
||||
logLevels: [LogLevel.warn, LogLevel.error],
|
||||
sortOrder: 'asc',
|
||||
page: 42,
|
||||
perPage: 146,
|
||||
signal,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/internal/detection_engine/rules/42/execution/events',
|
||||
{
|
||||
method: 'GET',
|
||||
query: {
|
||||
event_types: 'message',
|
||||
log_levels: 'warn,error',
|
||||
sort_order: 'asc',
|
||||
page: 42,
|
||||
per_page: 146,
|
||||
},
|
||||
signal,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchRuleExecutionResults', () => {
|
||||
const responseMock: GetRuleExecutionResultsResponse = {
|
||||
events: [],
|
||||
total: 0,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue(responseMock);
|
||||
});
|
||||
|
||||
it('calls API with correct parameters', async () => {
|
||||
await api.fetchRuleExecutionResults({
|
||||
ruleId: '42',
|
||||
start: '2001-01-01T17:00:00.000Z',
|
||||
end: '2001-01-02T17:00:00.000Z',
|
||||
queryText: '',
|
||||
statusFilters: [],
|
||||
signal,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/internal/detection_engine/rules/42/execution/results',
|
||||
{
|
||||
method: 'GET',
|
||||
query: {
|
||||
end: '2001-01-02T17:00:00.000Z',
|
||||
page: undefined,
|
||||
per_page: undefined,
|
||||
query_text: '',
|
||||
sort_field: undefined,
|
||||
sort_order: undefined,
|
||||
start: '2001-01-01T17:00:00.000Z',
|
||||
status_filters: '',
|
||||
},
|
||||
signal,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('returns API response as is', async () => {
|
||||
const response = await api.fetchRuleExecutionResults({
|
||||
ruleId: '42',
|
||||
start: 'now-30',
|
||||
end: 'now',
|
||||
queryText: '',
|
||||
statusFilters: [],
|
||||
signal,
|
||||
});
|
||||
expect(response).toEqual(responseMock);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 dateMath from '@kbn/datemath';
|
||||
|
||||
import { KibanaServices } from '../../../common/lib/kibana';
|
||||
|
||||
import type {
|
||||
GetRuleExecutionEventsResponse,
|
||||
GetRuleExecutionResultsResponse,
|
||||
} from '../../../../common/detection_engine/rule_monitoring';
|
||||
import {
|
||||
getRuleExecutionEventsUrl,
|
||||
getRuleExecutionResultsUrl,
|
||||
} from '../../../../common/detection_engine/rule_monitoring';
|
||||
|
||||
import type {
|
||||
FetchRuleExecutionEventsArgs,
|
||||
FetchRuleExecutionResultsArgs,
|
||||
IRuleMonitoringApiClient,
|
||||
} from './api_client_interface';
|
||||
|
||||
export const api: IRuleMonitoringApiClient = {
|
||||
fetchRuleExecutionEvents: (
|
||||
args: FetchRuleExecutionEventsArgs
|
||||
): Promise<GetRuleExecutionEventsResponse> => {
|
||||
const { ruleId, eventTypes, logLevels, sortOrder, page, perPage, signal } = args;
|
||||
|
||||
const url = getRuleExecutionEventsUrl(ruleId);
|
||||
|
||||
return http().fetch<GetRuleExecutionEventsResponse>(url, {
|
||||
method: 'GET',
|
||||
query: {
|
||||
event_types: eventTypes?.join(','),
|
||||
log_levels: logLevels?.join(','),
|
||||
sort_order: sortOrder,
|
||||
page,
|
||||
per_page: perPage,
|
||||
},
|
||||
signal,
|
||||
});
|
||||
},
|
||||
|
||||
fetchRuleExecutionResults: (
|
||||
args: FetchRuleExecutionResultsArgs
|
||||
): Promise<GetRuleExecutionResultsResponse> => {
|
||||
const {
|
||||
ruleId,
|
||||
start,
|
||||
end,
|
||||
queryText,
|
||||
statusFilters,
|
||||
page,
|
||||
perPage,
|
||||
sortField,
|
||||
sortOrder,
|
||||
signal,
|
||||
} = args;
|
||||
|
||||
const url = getRuleExecutionResultsUrl(ruleId);
|
||||
const startDate = dateMath.parse(start);
|
||||
const endDate = dateMath.parse(end, { roundUp: true });
|
||||
|
||||
return http().fetch<GetRuleExecutionResultsResponse>(url, {
|
||||
method: 'GET',
|
||||
query: {
|
||||
start: startDate?.utc().toISOString(),
|
||||
end: endDate?.utc().toISOString(),
|
||||
query_text: queryText,
|
||||
status_filters: statusFilters?.sort()?.join(','),
|
||||
sort_field: sortField,
|
||||
sort_order: sortOrder,
|
||||
page,
|
||||
per_page: perPage,
|
||||
},
|
||||
signal,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const http = () => KibanaServices.get().http;
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 { SortOrder } from '../../../../common/detection_engine/schemas/common';
|
||||
import type {
|
||||
GetRuleExecutionEventsResponse,
|
||||
GetRuleExecutionResultsResponse,
|
||||
LogLevel,
|
||||
RuleExecutionEventType,
|
||||
RuleExecutionResult,
|
||||
RuleExecutionStatus,
|
||||
} from '../../../../common/detection_engine/rule_monitoring';
|
||||
|
||||
export interface IRuleMonitoringApiClient {
|
||||
/**
|
||||
* Fetches plain rule execution events (status changes, metrics, generic events) from Event Log.
|
||||
* @throws An error if response is not OK.
|
||||
*/
|
||||
fetchRuleExecutionEvents(
|
||||
args: FetchRuleExecutionEventsArgs
|
||||
): Promise<GetRuleExecutionEventsResponse>;
|
||||
|
||||
/**
|
||||
* Fetches aggregated rule execution results (events grouped by execution UUID) from Event Log.
|
||||
* @throws An error if response is not OK.
|
||||
*/
|
||||
fetchRuleExecutionResults(
|
||||
args: FetchRuleExecutionResultsArgs
|
||||
): Promise<GetRuleExecutionResultsResponse>;
|
||||
}
|
||||
|
||||
export interface FetchRuleExecutionEventsArgs {
|
||||
/**
|
||||
* Saved Object ID of the rule (`rule.id`, not static `rule.rule_id`).
|
||||
*/
|
||||
ruleId: string;
|
||||
|
||||
/**
|
||||
* Filter by event type. If set, result will include only events matching any of these.
|
||||
*/
|
||||
eventTypes?: RuleExecutionEventType[];
|
||||
|
||||
/**
|
||||
* Filter by log level. If set, result will include only events matching any of these.
|
||||
*/
|
||||
logLevels?: LogLevel[];
|
||||
|
||||
/**
|
||||
* What order to sort by (e.g. `asc` or `desc`).
|
||||
*/
|
||||
sortOrder?: SortOrder;
|
||||
|
||||
/**
|
||||
* Current page to fetch.
|
||||
*/
|
||||
page?: number;
|
||||
|
||||
/**
|
||||
* Number of results to fetch per page.
|
||||
*/
|
||||
perPage?: number;
|
||||
|
||||
/**
|
||||
* Optional signal for cancelling the request.
|
||||
*/
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface FetchRuleExecutionResultsArgs {
|
||||
/**
|
||||
* Saved Object ID of the rule (`rule.id`, not static `rule.rule_id`).
|
||||
*/
|
||||
ruleId: string;
|
||||
|
||||
/**
|
||||
* Start daterange either in UTC ISO8601 or as datemath string (e.g. `2021-12-29T02:44:41.653Z` or `now-30`).
|
||||
*/
|
||||
start: string;
|
||||
|
||||
/**
|
||||
* End daterange either in UTC ISO8601 or as datemath string (e.g. `2021-12-29T02:44:41.653Z` or `now/w`).
|
||||
*/
|
||||
end: string;
|
||||
|
||||
/**
|
||||
* Search string in querystring format, e.g.
|
||||
* `event.duration > 1000 OR kibana.alert.rule.execution.metrics.execution_gap_duration_s > 100`.
|
||||
*/
|
||||
queryText?: string;
|
||||
|
||||
/**
|
||||
* Array of `statusFilters` (e.g. `succeeded,failed,partial failure`).
|
||||
*/
|
||||
statusFilters?: RuleExecutionStatus[];
|
||||
|
||||
/**
|
||||
* Keyof AggregateRuleExecutionEvent field to sort by.
|
||||
*/
|
||||
sortField?: keyof RuleExecutionResult;
|
||||
|
||||
/**
|
||||
* What order to sort by (e.g. `asc` or `desc`).
|
||||
*/
|
||||
sortOrder?: SortOrder;
|
||||
|
||||
/**
|
||||
* Current page to fetch.
|
||||
*/
|
||||
page?: number;
|
||||
|
||||
/**
|
||||
* Number of results to fetch per page.
|
||||
*/
|
||||
perPage?: number;
|
||||
|
||||
/**
|
||||
* Optional signal for cancelling the request.
|
||||
*/
|
||||
signal?: AbortSignal;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './api_client_interface';
|
||||
export * from './api_client';
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import type { RuleExecutionEventType } from '../../../../../../../common/detection_engine/rule_monitoring';
|
||||
import { RULE_EXECUTION_EVENT_TYPES } from '../../../../../../../common/detection_engine/rule_monitoring';
|
||||
import { EventTypeIndicator } from '../../indicators/event_type_indicator';
|
||||
import { MultiselectFilter } from '../multiselect_filter';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface EventTypeFilterProps {
|
||||
selectedItems: RuleExecutionEventType[];
|
||||
onChange: (selectedItems: RuleExecutionEventType[]) => void;
|
||||
}
|
||||
|
||||
const EventTypeFilterComponent: React.FC<EventTypeFilterProps> = ({ selectedItems, onChange }) => {
|
||||
const renderItem = useCallback((item: RuleExecutionEventType) => {
|
||||
return <EventTypeIndicator type={item} />;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MultiselectFilter<RuleExecutionEventType>
|
||||
dataTestSubj="eventTypeFilter"
|
||||
title={i18n.FILTER_TITLE}
|
||||
items={RULE_EXECUTION_EVENT_TYPES}
|
||||
selectedItems={selectedItems}
|
||||
onSelectionChange={onChange}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EventTypeFilter = React.memo(EventTypeFilterComponent);
|
||||
EventTypeFilter.displayName = 'EventTypeFilter';
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { buildRuleMessageFactory } from './rule_messages';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const buildRuleMessageMock = buildRuleMessageFactory({
|
||||
id: 'fake id',
|
||||
ruleId: 'fake rule id',
|
||||
index: 'fakeindex',
|
||||
name: 'fake name',
|
||||
});
|
||||
export const FILTER_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.eventTypeFilter.filterTitle',
|
||||
{
|
||||
defaultMessage: 'Event type',
|
||||
}
|
||||
);
|
|
@ -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 React, { useCallback } from 'react';
|
||||
|
||||
import type { RuleExecutionStatus } from '../../../../../../../common/detection_engine/rule_monitoring';
|
||||
import { ExecutionStatusIndicator } from '../../indicators/execution_status_indicator';
|
||||
import { MultiselectFilter } from '../multiselect_filter';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface ExecutionStatusFilterProps {
|
||||
items: RuleExecutionStatus[];
|
||||
selectedItems: RuleExecutionStatus[];
|
||||
onChange: (selectedItems: RuleExecutionStatus[]) => void;
|
||||
}
|
||||
|
||||
const ExecutionStatusFilterComponent: React.FC<ExecutionStatusFilterProps> = ({
|
||||
items,
|
||||
selectedItems,
|
||||
onChange,
|
||||
}) => {
|
||||
const renderItem = useCallback((item: RuleExecutionStatus) => {
|
||||
return <ExecutionStatusIndicator status={item} />;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MultiselectFilter<RuleExecutionStatus>
|
||||
dataTestSubj="ExecutionStatusFilter"
|
||||
title={i18n.FILTER_TITLE}
|
||||
items={items}
|
||||
selectedItems={selectedItems}
|
||||
onSelectionChange={onChange}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExecutionStatusFilter = React.memo(ExecutionStatusFilterComponent);
|
||||
ExecutionStatusFilter.displayName = 'ExecutionStatusFilter';
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { buildRuleMessageFactory } from '../rule_messages';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const mockBuildRuleMessage = buildRuleMessageFactory({
|
||||
id: 'fake id',
|
||||
ruleId: 'fake rule id',
|
||||
index: 'fakeindex',
|
||||
name: 'fake name',
|
||||
});
|
||||
export const FILTER_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.executionStatusFilter.filterTitle',
|
||||
{
|
||||
defaultMessage: 'Status',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import type { LogLevel } from '../../../../../../../common/detection_engine/rule_monitoring';
|
||||
import { LOG_LEVELS } from '../../../../../../../common/detection_engine/rule_monitoring';
|
||||
import { LogLevelIndicator } from '../../indicators/log_level_indicator';
|
||||
import { MultiselectFilter } from '../multiselect_filter';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface LogLevelFilterProps {
|
||||
selectedItems: LogLevel[];
|
||||
onChange: (selectedItems: LogLevel[]) => void;
|
||||
}
|
||||
|
||||
const LogLevelFilterComponent: React.FC<LogLevelFilterProps> = ({ selectedItems, onChange }) => {
|
||||
const renderItem = useCallback((item: LogLevel) => {
|
||||
return <LogLevelIndicator logLevel={item} />;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MultiselectFilter<LogLevel>
|
||||
dataTestSubj="logLevelFilter"
|
||||
title={i18n.FILTER_TITLE}
|
||||
items={LOG_LEVELS}
|
||||
selectedItems={selectedItems}
|
||||
onSelectionChange={onChange}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const LogLevelFilter = React.memo(LogLevelFilterComponent);
|
||||
LogLevelFilter.displayName = 'LogLevelFilter';
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const FILTER_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.logLevelFilter.filterTitle',
|
||||
{
|
||||
defaultMessage: 'Log level',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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 React, { useCallback, useMemo } from 'react';
|
||||
import { noop } from 'lodash';
|
||||
import { EuiPopover, EuiFilterGroup, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui';
|
||||
import { useBoolState } from '../../../../../../common/hooks/use_bool_state';
|
||||
|
||||
export interface MultiselectFilterProps<T = unknown> {
|
||||
dataTestSubj?: string;
|
||||
title: string;
|
||||
items: T[];
|
||||
selectedItems: T[];
|
||||
onSelectionChange?: (selectedItems: T[]) => void;
|
||||
renderItem?: (item: T) => React.ReactChild;
|
||||
renderLabel?: (item: T) => string;
|
||||
}
|
||||
|
||||
const MultiselectFilterComponent = <T extends unknown>(props: MultiselectFilterProps<T>) => {
|
||||
const { dataTestSubj, title, items, selectedItems, onSelectionChange, renderItem, renderLabel } =
|
||||
initializeProps(props);
|
||||
|
||||
const [isPopoverOpen, , closePopover, togglePopover] = useBoolState();
|
||||
|
||||
const handleItemClick = useCallback(
|
||||
(item: T) => {
|
||||
const newSelectedItems = selectedItems.includes(item)
|
||||
? selectedItems.filter((i) => i !== item)
|
||||
: [...selectedItems, item];
|
||||
onSelectionChange(newSelectedItems);
|
||||
},
|
||||
[selectedItems, onSelectionChange]
|
||||
);
|
||||
|
||||
const filterItemElements = useMemo(() => {
|
||||
return items.map((item, index) => {
|
||||
const itemLabel = renderLabel(item);
|
||||
const itemElement = renderItem(item);
|
||||
return (
|
||||
<EuiFilterSelectItem
|
||||
data-test-subj={`${dataTestSubj}-item`}
|
||||
title={itemLabel}
|
||||
key={`${index}-${itemLabel}`}
|
||||
checked={selectedItems.includes(item) ? 'on' : undefined}
|
||||
onClick={() => handleItemClick(item)}
|
||||
>
|
||||
{itemElement}
|
||||
</EuiFilterSelectItem>
|
||||
);
|
||||
});
|
||||
}, [dataTestSubj, items, selectedItems, renderItem, renderLabel, handleItemClick]);
|
||||
|
||||
return (
|
||||
<EuiFilterGroup data-test-subj={dataTestSubj}>
|
||||
<EuiPopover
|
||||
data-test-subj={`${dataTestSubj}-popover`}
|
||||
button={
|
||||
<EuiFilterButton
|
||||
data-test-subj={`${dataTestSubj}-popoverButton`}
|
||||
iconType="arrowDown"
|
||||
grow={false}
|
||||
numFilters={items.length}
|
||||
numActiveFilters={selectedItems.length}
|
||||
hasActiveFilters={selectedItems.length > 0}
|
||||
isSelected={isPopoverOpen}
|
||||
onClick={togglePopover}
|
||||
>
|
||||
{title}
|
||||
</EuiFilterButton>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
repositionOnScroll
|
||||
>
|
||||
{filterItemElements}
|
||||
</EuiPopover>
|
||||
</EuiFilterGroup>
|
||||
);
|
||||
};
|
||||
|
||||
// We have to wrap it in a function and cast to original type because React.memo
|
||||
// returns a component type which is not generic.
|
||||
const enhanceMultiselectFilterComponent = () => {
|
||||
const Component = React.memo(MultiselectFilterComponent);
|
||||
Component.displayName = 'MultiselectFilter';
|
||||
return Component as typeof MultiselectFilterComponent;
|
||||
};
|
||||
|
||||
export const MultiselectFilter = enhanceMultiselectFilterComponent();
|
||||
|
||||
const initializeProps = <T extends unknown>(
|
||||
props: MultiselectFilterProps<T>
|
||||
): Required<MultiselectFilterProps<T>> => {
|
||||
const onSelectionChange: (selectedItems: T[]) => void = props.onSelectionChange ?? noop;
|
||||
const renderLabel: (item: T) => string = props.renderLabel ?? String;
|
||||
const renderItem: (item: T) => React.ReactChild = props.renderItem ?? renderLabel;
|
||||
|
||||
return {
|
||||
dataTestSubj: props.dataTestSubj ?? 'multiselectFilter',
|
||||
title: props.title,
|
||||
items: props.items,
|
||||
selectedItems: props.selectedItems,
|
||||
onSelectionChange,
|
||||
renderLabel,
|
||||
renderItem,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiBadge } from '@elastic/eui';
|
||||
import type { RuleExecutionEventType } from '../../../../../../../common/detection_engine/rule_monitoring';
|
||||
import { getBadgeIcon, getBadgeText } from './utils';
|
||||
|
||||
interface EventTypeIndicatorProps {
|
||||
type: RuleExecutionEventType;
|
||||
}
|
||||
|
||||
const EventTypeIndicatorComponent: React.FC<EventTypeIndicatorProps> = ({ type }) => {
|
||||
const icon = getBadgeIcon(type);
|
||||
const text = getBadgeText(type);
|
||||
|
||||
return (
|
||||
<EuiBadge color="hollow" iconType={icon}>
|
||||
{text}
|
||||
</EuiBadge>
|
||||
);
|
||||
};
|
||||
|
||||
export const EventTypeIndicator = React.memo(EventTypeIndicatorComponent);
|
||||
EventTypeIndicator.displayName = 'EventTypeIndicator';
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const TYPE_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.eventTypeIndicator.messageText',
|
||||
{
|
||||
defaultMessage: 'Message',
|
||||
}
|
||||
);
|
||||
|
||||
export const TYPE_STATUS_CHANGE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.eventTypeIndicator.statusChangeText',
|
||||
{
|
||||
defaultMessage: 'Status',
|
||||
}
|
||||
);
|
||||
|
||||
export const TYPE_EXECUTION_METRICS = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.eventTypeIndicator.executionMetricsText',
|
||||
{
|
||||
defaultMessage: 'Metrics',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { IconType } from '@elastic/eui';
|
||||
import { RuleExecutionEventType } from '../../../../../../../common/detection_engine/rule_monitoring';
|
||||
import { assertUnreachable } from '../../../../../../../common/utility_types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const getBadgeIcon = (type: RuleExecutionEventType): IconType => {
|
||||
switch (type) {
|
||||
case RuleExecutionEventType.message:
|
||||
return 'console';
|
||||
case RuleExecutionEventType['status-change']:
|
||||
return 'dot';
|
||||
case RuleExecutionEventType['execution-metrics']:
|
||||
return 'gear';
|
||||
default:
|
||||
return assertUnreachable(type, 'Unknown rule execution event type');
|
||||
}
|
||||
};
|
||||
|
||||
export const getBadgeText = (type: RuleExecutionEventType): string => {
|
||||
switch (type) {
|
||||
case RuleExecutionEventType.message:
|
||||
return i18n.TYPE_MESSAGE;
|
||||
case RuleExecutionEventType['status-change']:
|
||||
return i18n.TYPE_STATUS_CHANGE;
|
||||
case RuleExecutionEventType['execution-metrics']:
|
||||
return i18n.TYPE_EXECUTION_METRICS;
|
||||
default:
|
||||
return assertUnreachable(type, 'Unknown rule execution event type');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiHealth } from '@elastic/eui';
|
||||
|
||||
import type { RuleExecutionStatus } from '../../../../../../../common/detection_engine/rule_monitoring';
|
||||
import { getEmptyTagValue } from '../../../../../../common/components/empty_value';
|
||||
import { RuleStatusBadge } from '../../../../../../detections/components/rules/rule_execution_status';
|
||||
import {
|
||||
getCapitalizedStatusText,
|
||||
getStatusColor,
|
||||
} from '../../../../../../detections/components/rules/rule_execution_status/utils';
|
||||
|
||||
const EMPTY_STATUS_TEXT = getEmptyTagValue();
|
||||
|
||||
interface ExecutionStatusIndicatorProps {
|
||||
status?: RuleExecutionStatus | null | undefined;
|
||||
showTooltip?: boolean;
|
||||
}
|
||||
|
||||
const ExecutionStatusIndicatorComponent: React.FC<ExecutionStatusIndicatorProps> = ({
|
||||
status,
|
||||
showTooltip = false,
|
||||
}) => {
|
||||
const statusText = getCapitalizedStatusText(status) ?? EMPTY_STATUS_TEXT;
|
||||
const statusColor = getStatusColor(status);
|
||||
|
||||
return showTooltip ? (
|
||||
<RuleStatusBadge status={status} />
|
||||
) : (
|
||||
<EuiHealth color={statusColor}>{statusText}</EuiHealth>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExecutionStatusIndicator = React.memo(ExecutionStatusIndicatorComponent);
|
||||
ExecutionStatusIndicator.displayName = 'ExecutionStatusIndicator';
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiBadge } from '@elastic/eui';
|
||||
import type { LogLevel } from '../../../../../../../common/detection_engine/rule_monitoring';
|
||||
import { getBadgeColor, getBadgeText } from './utils';
|
||||
|
||||
interface LogLevelIndicatorProps {
|
||||
logLevel: LogLevel;
|
||||
}
|
||||
|
||||
const LogLevelIndicatorComponent: React.FC<LogLevelIndicatorProps> = ({ logLevel }) => {
|
||||
const color = getBadgeColor(logLevel);
|
||||
const text = getBadgeText(logLevel);
|
||||
|
||||
return <EuiBadge color={color}>{text}</EuiBadge>;
|
||||
};
|
||||
|
||||
export const LogLevelIndicator = React.memo(LogLevelIndicatorComponent);
|
||||
LogLevelIndicator.displayName = 'LogLevelIndicator';
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { upperCase } from 'lodash';
|
||||
import type { IconColor } from '@elastic/eui';
|
||||
import { LogLevel } from '../../../../../../../common/detection_engine/rule_monitoring';
|
||||
import { assertUnreachable } from '../../../../../../../common/utility_types';
|
||||
|
||||
export const getBadgeColor = (logLevel: LogLevel): IconColor => {
|
||||
switch (logLevel) {
|
||||
case LogLevel.trace:
|
||||
return 'hollow';
|
||||
case LogLevel.debug:
|
||||
return 'hollow';
|
||||
case LogLevel.info:
|
||||
return 'default';
|
||||
case LogLevel.warn:
|
||||
return 'warning';
|
||||
case LogLevel.error:
|
||||
return 'danger';
|
||||
default:
|
||||
return assertUnreachable(logLevel, 'Unknown log level');
|
||||
}
|
||||
};
|
||||
|
||||
export const getBadgeText = (logLevel: LogLevel): string => {
|
||||
return upperCase(logLevel);
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
type TableItem = Record<string, unknown>;
|
||||
type TableItemId = string;
|
||||
type TableItemRowMap = Record<TableItemId, React.ReactNode>;
|
||||
|
||||
interface UseExpandableRowsArgs<T> {
|
||||
getItemId: (item: T) => TableItemId;
|
||||
renderItem: (item: T) => React.ReactChild;
|
||||
}
|
||||
|
||||
export const useExpandableRows = <T extends TableItem>(args: UseExpandableRowsArgs<T>) => {
|
||||
const { getItemId, renderItem } = args;
|
||||
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<TableItemRowMap>({});
|
||||
|
||||
const toggleRowExpanded = useCallback(
|
||||
(item: T) => {
|
||||
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
|
||||
const itemId = getItemId(item);
|
||||
|
||||
if (itemIdToExpandedRowMapValues[itemId]) {
|
||||
delete itemIdToExpandedRowMapValues[itemId];
|
||||
} else {
|
||||
itemIdToExpandedRowMapValues[itemId] = renderItem(item);
|
||||
}
|
||||
|
||||
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
|
||||
},
|
||||
[itemIdToExpandedRowMap, getItemId, renderItem]
|
||||
);
|
||||
|
||||
const isRowExpanded = useCallback(
|
||||
(item: T): boolean => {
|
||||
const itemId = getItemId(item);
|
||||
return itemIdToExpandedRowMap[itemId] != null;
|
||||
},
|
||||
[itemIdToExpandedRowMap, getItemId]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
itemIdToExpandedRowMap,
|
||||
getItemId,
|
||||
toggleRowExpanded,
|
||||
isRowExpanded,
|
||||
};
|
||||
}, [itemIdToExpandedRowMap, getItemId, toggleRowExpanded, isRowExpanded]);
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import type { CriteriaWithPagination } from '@elastic/eui';
|
||||
|
||||
const DEFAULT_PAGE_SIZE_OPTIONS = [10, 20, 50];
|
||||
const DEFAULT_PAGE_NUMBER = 1;
|
||||
|
||||
interface UsePaginationArgs {
|
||||
pageSizeOptions?: number[];
|
||||
pageSizeDefault?: number;
|
||||
pageNumberDefault?: number;
|
||||
}
|
||||
|
||||
type TableItem = Record<string, unknown>;
|
||||
|
||||
export const usePagination = <T extends TableItem>(args: UsePaginationArgs) => {
|
||||
const pageSizeOptions = args.pageSizeOptions ?? DEFAULT_PAGE_SIZE_OPTIONS;
|
||||
const pageSizeDefault = args.pageSizeDefault ?? pageSizeOptions[0];
|
||||
const pageNumberDefault = args.pageNumberDefault ?? DEFAULT_PAGE_NUMBER;
|
||||
|
||||
const [pageSize, setPageSize] = useState(pageSizeDefault);
|
||||
const [pageNumber, setPageNumber] = useState(pageNumberDefault);
|
||||
const [totalItemCount, setTotalItemCount] = useState(0);
|
||||
|
||||
const state = useMemo(() => {
|
||||
return {
|
||||
pageSizeOptions,
|
||||
pageSize,
|
||||
pageNumber,
|
||||
pageIndex: pageNumber - 1,
|
||||
totalItemCount,
|
||||
};
|
||||
}, [pageSizeOptions, pageSize, pageNumber, totalItemCount]);
|
||||
|
||||
const update = useCallback(
|
||||
(criteria: CriteriaWithPagination<T>): void => {
|
||||
setPageNumber(criteria.page.index + 1);
|
||||
setPageSize(criteria.page.size);
|
||||
},
|
||||
[setPageNumber, setPageSize]
|
||||
);
|
||||
|
||||
const updateTotalItemCount = useCallback(
|
||||
(count: number | null | undefined): void => {
|
||||
setTotalItemCount(count ?? 0);
|
||||
},
|
||||
[setTotalItemCount]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({ state, update, updateTotalItemCount }),
|
||||
[state, update, updateTotalItemCount]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { useCallback, useMemo, useState } from 'react';
|
||||
import type { CriteriaWithPagination } from '@elastic/eui';
|
||||
import type { SortOrder } from '../../../../../../common/detection_engine/schemas/common';
|
||||
|
||||
type TableItem = Record<string, unknown>;
|
||||
|
||||
export const useSorting = <T extends TableItem>(defaultField: keyof T, defaultOrder: SortOrder) => {
|
||||
const [sortField, setSortField] = useState<keyof T>(defaultField);
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>(defaultOrder);
|
||||
|
||||
const state = useMemo(() => {
|
||||
return {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortOrder,
|
||||
},
|
||||
};
|
||||
}, [sortField, sortOrder]);
|
||||
|
||||
const update = useCallback(
|
||||
(criteria: CriteriaWithPagination<T>): void => {
|
||||
if (criteria.sort) {
|
||||
setSortField(criteria.sort.field);
|
||||
setSortOrder(criteria.sort.direction);
|
||||
}
|
||||
},
|
||||
[setSortField, setSortOrder]
|
||||
);
|
||||
|
||||
return useMemo(() => ({ state, update }), [state, update]);
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiCodeBlock } from '@elastic/eui';
|
||||
|
||||
const DEFAULT_OVERFLOW_HEIGHT = 320;
|
||||
|
||||
interface TextBlockProps {
|
||||
text: string | null | undefined;
|
||||
}
|
||||
|
||||
const TextBlockComponent: React.FC<TextBlockProps> = ({ text }) => {
|
||||
return (
|
||||
<EuiCodeBlock
|
||||
className="eui-fullWidth"
|
||||
isCopyable
|
||||
overflowHeight={DEFAULT_OVERFLOW_HEIGHT}
|
||||
transparentBackground
|
||||
>
|
||||
{text ?? ''}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export const TextBlock = React.memo(TextBlockComponent);
|
||||
TextBlock.displayName = 'TextBlock';
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface TruncatedTextProps {
|
||||
text: string | null | undefined;
|
||||
}
|
||||
|
||||
const TruncatedTextComponent: React.FC<TruncatedTextProps> = ({ text }) => {
|
||||
return text != null ? <span className="eui-fullWidth eui-textTruncate">{text}</span> : null;
|
||||
};
|
||||
|
||||
export const TruncatedText = React.memo(TruncatedTextComponent);
|
||||
TruncatedText.displayName = 'TruncatedText';
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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 React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import type { CriteriaWithPagination } from '@elastic/eui';
|
||||
import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
|
||||
import type { RuleExecutionEvent } from '../../../../../common/detection_engine/rule_monitoring';
|
||||
|
||||
import { HeaderSection } from '../../../../common/components/header_section';
|
||||
import { EventTypeFilter } from '../basic/filters/event_type_filter';
|
||||
import { LogLevelFilter } from '../basic/filters/log_level_filter';
|
||||
import { ExecutionEventsTableRowDetails } from './execution_events_table_row_details';
|
||||
|
||||
import { useFilters } from './use_filters';
|
||||
import { useSorting } from '../basic/tables/use_sorting';
|
||||
import { usePagination } from '../basic/tables/use_pagination';
|
||||
import { useColumns } from './use_columns';
|
||||
import { useExpandableRows } from '../basic/tables/use_expandable_rows';
|
||||
import { useExecutionEvents } from './use_execution_events';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [10, 20, 50, 100, 200];
|
||||
|
||||
interface ExecutionEventsTableProps {
|
||||
ruleId: string;
|
||||
}
|
||||
|
||||
const ExecutionEventsTableComponent: React.FC<ExecutionEventsTableProps> = ({ ruleId }) => {
|
||||
const getItemId = useCallback((item: RuleExecutionEvent): string => {
|
||||
return `${item.timestamp} ${item.sequence}`;
|
||||
}, []);
|
||||
|
||||
const renderExpandedItem = useCallback((item: RuleExecutionEvent) => {
|
||||
return <ExecutionEventsTableRowDetails item={item} />;
|
||||
}, []);
|
||||
|
||||
const rows = useExpandableRows<RuleExecutionEvent>({
|
||||
getItemId,
|
||||
renderItem: renderExpandedItem,
|
||||
});
|
||||
|
||||
const columns = useColumns({
|
||||
toggleRowExpanded: rows.toggleRowExpanded,
|
||||
isRowExpanded: rows.isRowExpanded,
|
||||
});
|
||||
|
||||
const filters = useFilters();
|
||||
const sorting = useSorting<RuleExecutionEvent>('timestamp', 'desc');
|
||||
const pagination = usePagination<RuleExecutionEvent>({ pageSizeOptions: PAGE_SIZE_OPTIONS });
|
||||
|
||||
const executionEvents = useExecutionEvents({
|
||||
ruleId,
|
||||
eventTypes: filters.state.eventTypes,
|
||||
logLevels: filters.state.logLevels,
|
||||
sortOrder: sorting.state.sort.direction,
|
||||
page: pagination.state.pageNumber,
|
||||
perPage: pagination.state.pageSize,
|
||||
});
|
||||
|
||||
// Each time execution events are fetched
|
||||
useEffect(() => {
|
||||
// We need to update total item count for the pagination to work properly
|
||||
pagination.updateTotalItemCount(executionEvents.data?.pagination.total);
|
||||
}, [executionEvents, pagination]);
|
||||
|
||||
const items = useMemo(() => executionEvents.data?.events ?? [], [executionEvents.data]);
|
||||
|
||||
const handleTableChange = useCallback(
|
||||
(criteria: CriteriaWithPagination<RuleExecutionEvent>): void => {
|
||||
sorting.update(criteria);
|
||||
pagination.update(criteria);
|
||||
},
|
||||
[sorting, pagination]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder>
|
||||
{/* Filter bar */}
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={true}>
|
||||
<HeaderSection title={i18n.TABLE_TITLE} subtitle={i18n.TABLE_SUBTITLE} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<LogLevelFilter selectedItems={filters.state.logLevels} onChange={filters.setLogLevels} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EventTypeFilter
|
||||
selectedItems={filters.state.eventTypes}
|
||||
onChange={filters.setEventTypes}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{/* Table with items */}
|
||||
<EuiBasicTable
|
||||
columns={columns}
|
||||
items={items}
|
||||
itemId={getItemId}
|
||||
itemIdToExpandedRowMap={rows.itemIdToExpandedRowMap}
|
||||
isExpandable={true}
|
||||
loading={executionEvents.isFetching}
|
||||
sorting={sorting.state}
|
||||
pagination={pagination.state}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExecutionEventsTable = React.memo(ExecutionEventsTableComponent);
|
||||
ExecutionEventsTable.displayName = 'RuleExecutionEventsTable';
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiDescriptionList } from '@elastic/eui';
|
||||
import type { RuleExecutionEvent } from '../../../../../common/detection_engine/rule_monitoring';
|
||||
import { TextBlock } from '../basic/text/text_block';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface ExecutionEventsTableRowDetailsProps {
|
||||
item: RuleExecutionEvent;
|
||||
}
|
||||
|
||||
const ExecutionEventsTableRowDetailsComponent: React.FC<ExecutionEventsTableRowDetailsProps> = ({
|
||||
item,
|
||||
}) => {
|
||||
return (
|
||||
<EuiDescriptionList
|
||||
className="eui-fullWidth"
|
||||
listItems={[
|
||||
{
|
||||
title: i18n.ROW_DETAILS_MESSAGE,
|
||||
description: <TextBlock text={item.message} />,
|
||||
},
|
||||
{
|
||||
title: i18n.ROW_DETAILS_JSON,
|
||||
description: <TextBlock text={JSON.stringify(item, null, 2)} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExecutionEventsTableRowDetails = React.memo(ExecutionEventsTableRowDetailsComponent);
|
||||
ExecutionEventsTableRowDetails.displayName = 'ExecutionEventsTableRowDetails';
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const TABLE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.tableTitle',
|
||||
{
|
||||
defaultMessage: 'Execution log',
|
||||
}
|
||||
);
|
||||
|
||||
export const TABLE_SUBTITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.tableSubtitle',
|
||||
{
|
||||
defaultMessage: 'A detailed log of rule execution events',
|
||||
}
|
||||
);
|
||||
|
||||
export const COLUMN_TIMESTAMP = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.timestampColumn',
|
||||
{
|
||||
defaultMessage: 'Timestamp',
|
||||
}
|
||||
);
|
||||
|
||||
export const COLUMN_LOG_LEVEL = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.logLevelColumn',
|
||||
{
|
||||
defaultMessage: 'Level',
|
||||
}
|
||||
);
|
||||
|
||||
export const COLUMN_EVENT_TYPE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.eventTypeColumn',
|
||||
{
|
||||
defaultMessage: 'Type',
|
||||
}
|
||||
);
|
||||
|
||||
export const COLUMN_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.messageColumn',
|
||||
{
|
||||
defaultMessage: 'Message',
|
||||
}
|
||||
);
|
||||
|
||||
export const FETCH_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.fetchErrorDescription',
|
||||
{
|
||||
defaultMessage: 'Failed to fetch rule execution events',
|
||||
}
|
||||
);
|
||||
|
||||
export const ROW_DETAILS_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.rowDetails.messageTitle',
|
||||
{
|
||||
defaultMessage: 'Message',
|
||||
}
|
||||
);
|
||||
|
||||
export const ROW_DETAILS_JSON = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.rowDetails.jsonTitle',
|
||||
{
|
||||
defaultMessage: 'Full JSON',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 React, { useMemo } from 'react';
|
||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { EuiButtonIcon, EuiScreenReaderOnly, RIGHT_ALIGNMENT } from '@elastic/eui';
|
||||
|
||||
import type {
|
||||
LogLevel,
|
||||
RuleExecutionEvent,
|
||||
RuleExecutionEventType,
|
||||
} from '../../../../../common/detection_engine/rule_monitoring';
|
||||
|
||||
import { FormattedDate } from '../../../../common/components/formatted_date';
|
||||
import { EventTypeIndicator } from '../basic/indicators/event_type_indicator';
|
||||
import { LogLevelIndicator } from '../basic/indicators/log_level_indicator';
|
||||
import { TruncatedText } from '../basic/text/truncated_text';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
type TableColumn = EuiBasicTableColumn<RuleExecutionEvent>;
|
||||
|
||||
interface UseColumnsArgs {
|
||||
toggleRowExpanded: (item: RuleExecutionEvent) => void;
|
||||
isRowExpanded: (item: RuleExecutionEvent) => boolean;
|
||||
}
|
||||
|
||||
export const useColumns = (args: UseColumnsArgs): TableColumn[] => {
|
||||
const { toggleRowExpanded, isRowExpanded } = args;
|
||||
|
||||
return useMemo(() => {
|
||||
return [
|
||||
timestampColumn,
|
||||
logLevelColumn,
|
||||
eventTypeColumn,
|
||||
messageColumn,
|
||||
expanderColumn({ toggleRowExpanded, isRowExpanded }),
|
||||
];
|
||||
}, [toggleRowExpanded, isRowExpanded]);
|
||||
};
|
||||
|
||||
const timestampColumn: TableColumn = {
|
||||
field: 'timestamp',
|
||||
name: i18n.COLUMN_TIMESTAMP,
|
||||
render: (value: string) => <FormattedDate value={value} fieldName="timestamp" />,
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '20%',
|
||||
};
|
||||
|
||||
const logLevelColumn: TableColumn = {
|
||||
field: 'level',
|
||||
name: i18n.COLUMN_LOG_LEVEL,
|
||||
render: (value: LogLevel) => <LogLevelIndicator logLevel={value} />,
|
||||
sortable: false,
|
||||
truncateText: false,
|
||||
width: '8%',
|
||||
};
|
||||
|
||||
const eventTypeColumn: TableColumn = {
|
||||
field: 'type',
|
||||
name: i18n.COLUMN_EVENT_TYPE,
|
||||
render: (value: RuleExecutionEventType) => <EventTypeIndicator type={value} />,
|
||||
sortable: false,
|
||||
truncateText: false,
|
||||
width: '8%',
|
||||
};
|
||||
|
||||
const messageColumn: TableColumn = {
|
||||
field: 'message',
|
||||
name: i18n.COLUMN_MESSAGE,
|
||||
render: (value: string) => <TruncatedText text={value} />,
|
||||
sortable: false,
|
||||
truncateText: true,
|
||||
width: '64%',
|
||||
};
|
||||
|
||||
const expanderColumn = ({ toggleRowExpanded, isRowExpanded }: UseColumnsArgs): TableColumn => {
|
||||
return {
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
name: (
|
||||
<EuiScreenReaderOnly>
|
||||
<span>{'Expand rows'}</span>
|
||||
</EuiScreenReaderOnly>
|
||||
),
|
||||
render: (item: RuleExecutionEvent) => (
|
||||
<EuiButtonIcon
|
||||
onClick={() => toggleRowExpanded(item)}
|
||||
aria-label={isRowExpanded(item) ? 'Collapse' : 'Expand'}
|
||||
iconType={isRowExpanded(item) ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { renderHook, cleanup } from '@testing-library/react-hooks';
|
||||
|
||||
import {
|
||||
LogLevel,
|
||||
RuleExecutionEventType,
|
||||
} from '../../../../../common/detection_engine/rule_monitoring';
|
||||
|
||||
import { useExecutionEvents } from './use_execution_events';
|
||||
import { useToasts } from '../../../../common/lib/kibana';
|
||||
import { api } from '../../api';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../api');
|
||||
|
||||
const SOME_RULE_ID = 'some-rule-id';
|
||||
|
||||
describe('useExecutionEvents', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const createReactQueryWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// Turn retries off, otherwise we won't be able to test errors
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
const wrapper: React.FC = ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
const render = () =>
|
||||
renderHook(() => useExecutionEvents({ ruleId: SOME_RULE_ID }), {
|
||||
wrapper: createReactQueryWrapper(),
|
||||
});
|
||||
|
||||
it('calls the API via fetchRuleExecutionEvents', async () => {
|
||||
const fetchRuleExecutionEvents = jest.spyOn(api, 'fetchRuleExecutionEvents');
|
||||
|
||||
const { waitForNextUpdate } = render();
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(fetchRuleExecutionEvents).toHaveBeenCalledTimes(1);
|
||||
expect(fetchRuleExecutionEvents).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ ruleId: SOME_RULE_ID })
|
||||
);
|
||||
});
|
||||
|
||||
it('fetches data from the API', async () => {
|
||||
const { result, waitForNextUpdate } = render();
|
||||
|
||||
// It starts from a loading state
|
||||
expect(result.current.isLoading).toEqual(true);
|
||||
expect(result.current.isSuccess).toEqual(false);
|
||||
expect(result.current.isError).toEqual(false);
|
||||
|
||||
// When fetchRuleExecutionEvents returns
|
||||
await waitForNextUpdate();
|
||||
|
||||
// It switches to a success state
|
||||
expect(result.current.isLoading).toEqual(false);
|
||||
expect(result.current.isSuccess).toEqual(true);
|
||||
expect(result.current.isError).toEqual(false);
|
||||
expect(result.current.data).toEqual({
|
||||
events: [
|
||||
{
|
||||
timestamp: '2021-12-29T10:42:59.996Z',
|
||||
sequence: 0,
|
||||
level: LogLevel.info,
|
||||
type: RuleExecutionEventType['status-change'],
|
||||
message: 'Rule changed status to "succeeded". Rule execution completed without errors',
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('handles exceptions from the API', async () => {
|
||||
const exception = new Error('Boom!');
|
||||
jest.spyOn(api, 'fetchRuleExecutionEvents').mockRejectedValue(exception);
|
||||
|
||||
const { result, waitForNextUpdate } = render();
|
||||
|
||||
// It starts from a loading state
|
||||
expect(result.current.isLoading).toEqual(true);
|
||||
expect(result.current.isSuccess).toEqual(false);
|
||||
expect(result.current.isError).toEqual(false);
|
||||
|
||||
// When fetchRuleExecutionEvents throws
|
||||
await waitForNextUpdate();
|
||||
|
||||
// It switches to an error state
|
||||
expect(result.current.isLoading).toEqual(false);
|
||||
expect(result.current.isSuccess).toEqual(false);
|
||||
expect(result.current.isError).toEqual(true);
|
||||
expect(result.current.error).toEqual(exception);
|
||||
|
||||
// And shows a toast with the caught exception
|
||||
expect(useToasts().addError).toHaveBeenCalledTimes(1);
|
||||
expect(useToasts().addError).toHaveBeenCalledWith(exception, {
|
||||
title: 'Failed to fetch rule execution events',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 { useQuery } from 'react-query';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
|
||||
import type { GetRuleExecutionEventsResponse } from '../../../../../common/detection_engine/rule_monitoring';
|
||||
import type { FetchRuleExecutionEventsArgs } from '../../api';
|
||||
import { api } from '../../api';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
export type UseExecutionEventsArgs = Omit<FetchRuleExecutionEventsArgs, 'signal'>;
|
||||
|
||||
export const useExecutionEvents = (args: UseExecutionEventsArgs) => {
|
||||
const { addError } = useAppToasts();
|
||||
|
||||
return useQuery<GetRuleExecutionEventsResponse>(
|
||||
['detectionEngine', 'ruleMonitoring', 'executionEvents', args],
|
||||
({ signal }) => {
|
||||
return api.fetchRuleExecutionEvents({ ...args, signal });
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
refetchInterval: 20000,
|
||||
onError: (e) => {
|
||||
addError(e, { title: i18n.FETCH_ERROR });
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type {
|
||||
LogLevel,
|
||||
RuleExecutionEventType,
|
||||
} from '../../../../../common/detection_engine/rule_monitoring';
|
||||
|
||||
export const useFilters = () => {
|
||||
const [logLevels, setLogLevels] = useState<LogLevel[]>([]);
|
||||
const [eventTypes, setEventTypes] = useState<RuleExecutionEventType[]>([]);
|
||||
|
||||
const state = useMemo(() => ({ logLevels, eventTypes }), [logLevels, eventTypes]);
|
||||
|
||||
return useMemo(
|
||||
() => ({ state, setLogLevels, setEventTypes }),
|
||||
[state, setLogLevels, setEventTypes]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const FETCH_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleMonitoring.executionResultsTable.fetchErrorDescription',
|
||||
{
|
||||
defaultMessage: 'Failed to fetch rule execution results',
|
||||
}
|
||||
);
|
|
@ -5,21 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
jest.mock('./api');
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
import React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { renderHook, cleanup } from '@testing-library/react-hooks';
|
||||
|
||||
import { useRuleExecutionEvents } from './use_rule_execution_events';
|
||||
|
||||
import * as api from './api';
|
||||
import { useExecutionResults } from './use_execution_results';
|
||||
import { useToasts } from '../../../../common/lib/kibana';
|
||||
import { api } from '../../api';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../api');
|
||||
|
||||
const SOME_RULE_ID = 'some-rule-id';
|
||||
|
||||
describe('useRuleExecutionEvents', () => {
|
||||
describe('useExecutionResults', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
@ -46,7 +45,7 @@ describe('useRuleExecutionEvents', () => {
|
|||
const render = () =>
|
||||
renderHook(
|
||||
() =>
|
||||
useRuleExecutionEvents({
|
||||
useExecutionResults({
|
||||
ruleId: SOME_RULE_ID,
|
||||
start: 'now-30',
|
||||
end: 'now',
|
||||
|
@ -58,15 +57,15 @@ describe('useRuleExecutionEvents', () => {
|
|||
}
|
||||
);
|
||||
|
||||
it('calls the API via fetchRuleExecutionEvents', async () => {
|
||||
const fetchRuleExecutionEvents = jest.spyOn(api, 'fetchRuleExecutionEvents');
|
||||
it('calls the API via fetchRuleExecutionResults', async () => {
|
||||
const fetchRuleExecutionResults = jest.spyOn(api, 'fetchRuleExecutionResults');
|
||||
|
||||
const { waitForNextUpdate } = render();
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(fetchRuleExecutionEvents).toHaveBeenCalledTimes(1);
|
||||
expect(fetchRuleExecutionEvents).toHaveBeenLastCalledWith(
|
||||
expect(fetchRuleExecutionResults).toHaveBeenCalledTimes(1);
|
||||
expect(fetchRuleExecutionResults).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ ruleId: SOME_RULE_ID })
|
||||
);
|
||||
});
|
||||
|
@ -118,7 +117,7 @@ describe('useRuleExecutionEvents', () => {
|
|||
|
||||
it('handles exceptions from the API', async () => {
|
||||
const exception = new Error('Boom!');
|
||||
jest.spyOn(api, 'fetchRuleExecutionEvents').mockRejectedValue(exception);
|
||||
jest.spyOn(api, 'fetchRuleExecutionResults').mockRejectedValue(exception);
|
||||
|
||||
const { result, waitForNextUpdate } = render();
|
||||
|
||||
|
@ -139,7 +138,7 @@ describe('useRuleExecutionEvents', () => {
|
|||
// And shows a toast with the caught exception
|
||||
expect(useToasts().addError).toHaveBeenCalledTimes(1);
|
||||
expect(useToasts().addError).toHaveBeenCalledWith(exception, {
|
||||
title: 'Failed to fetch rule execution events',
|
||||
title: 'Failed to fetch rule execution results',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useQuery } from 'react-query';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
|
||||
import type { GetRuleExecutionResultsResponse } from '../../../../../common/detection_engine/rule_monitoring';
|
||||
import type { FetchRuleExecutionResultsArgs } from '../../api';
|
||||
import { api } from '../../api';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
export type UseExecutionResultsArgs = Omit<FetchRuleExecutionResultsArgs, 'signal'>;
|
||||
|
||||
export const useExecutionResults = (args: UseExecutionResultsArgs) => {
|
||||
const { addError } = useAppToasts();
|
||||
|
||||
return useQuery<GetRuleExecutionResultsResponse>(
|
||||
['detectionEngine', 'ruleMonitoring', 'executionResults', args],
|
||||
({ signal }) => {
|
||||
return api.fetchRuleExecutionResults({ ...args, signal });
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
onError: (e) => {
|
||||
addError(e, { title: i18n.FETCH_ERROR });
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './api';
|
||||
|
||||
export * from './components/basic/filters/execution_status_filter';
|
||||
export * from './components/basic/indicators/execution_status_indicator';
|
||||
export * from './components/execution_events_table/execution_events_table';
|
||||
export * from './components/execution_results_table/use_execution_results';
|
||||
|
||||
export * from './logic/execution_settings/use_execution_settings';
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { useUiSetting$ } from '../../../../common/lib/kibana';
|
||||
|
||||
import {
|
||||
EXTENDED_RULE_EXECUTION_LOGGING_ENABLED_SETTING,
|
||||
EXTENDED_RULE_EXECUTION_LOGGING_MIN_LEVEL_SETTING,
|
||||
} from '../../../../../common/constants';
|
||||
import type { RuleExecutionSettings } from '../../../../../common/detection_engine/rule_monitoring';
|
||||
import { LogLevelSetting } from '../../../../../common/detection_engine/rule_monitoring';
|
||||
|
||||
export const useRuleExecutionSettings = (): RuleExecutionSettings => {
|
||||
const featureFlagEnabled = useIsExperimentalFeatureEnabled('extendedRuleExecutionLoggingEnabled');
|
||||
|
||||
const advancedSettingEnabled = useAdvancedSettingSafely<boolean>(
|
||||
EXTENDED_RULE_EXECUTION_LOGGING_ENABLED_SETTING,
|
||||
false
|
||||
);
|
||||
const advancedSettingMinLevel = useAdvancedSettingSafely<LogLevelSetting>(
|
||||
EXTENDED_RULE_EXECUTION_LOGGING_MIN_LEVEL_SETTING,
|
||||
LogLevelSetting.off
|
||||
);
|
||||
|
||||
return useMemo<RuleExecutionSettings>(() => {
|
||||
return {
|
||||
extendedLogging: {
|
||||
isEnabled: featureFlagEnabled && advancedSettingEnabled,
|
||||
minLevel: advancedSettingMinLevel,
|
||||
},
|
||||
};
|
||||
}, [featureFlagEnabled, advancedSettingEnabled, advancedSettingMinLevel]);
|
||||
};
|
||||
|
||||
const useAdvancedSettingSafely = <T>(key: string, defaultValue: T): T => {
|
||||
try {
|
||||
const [value] = useUiSetting$<T>(key);
|
||||
return value;
|
||||
} catch (e) {
|
||||
// It throws when the setting is not registered (when featureFlagEnabled === false).
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './api/__mocks__';
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import { EuiFlexItem, EuiHealth, EuiText } from '@elastic/eui';
|
||||
|
||||
import type { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common';
|
||||
import type { RuleExecutionStatus } from '../../../../../common/detection_engine/rule_monitoring';
|
||||
|
||||
import { FormattedDate } from '../../../../common/components/formatted_date';
|
||||
import { getEmptyTagValue } from '../../../../common/components/empty_value';
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common';
|
||||
import { RuleExecutionStatus } from '../../../../../common/detection_engine/rule_monitoring';
|
||||
import { RuleStatusBadge } from './rule_status_badge';
|
||||
|
||||
describe('RuleStatusBadge', () => {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { getEmptyTagValue } from '../../../../common/components/empty_value';
|
|||
import { HealthTruncateText } from '../../../../common/components/health_truncate_text';
|
||||
import { getCapitalizedStatusText, getStatusColor } from './utils';
|
||||
|
||||
import type { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common';
|
||||
import type { RuleExecutionStatus } from '../../../../../common/detection_engine/rule_monitoring';
|
||||
|
||||
interface RuleStatusBadgeProps {
|
||||
status: RuleExecutionStatus | null | undefined;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common';
|
||||
import { RuleExecutionStatus } from '../../../../../common/detection_engine/rule_monitoring';
|
||||
import { RuleStatusFailedCallOut } from './rule_status_failed_callout';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
|
||||
import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { FormattedDate } from '../../../../common/components/formatted_date';
|
||||
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common';
|
||||
import { RuleExecutionStatus } from '../../../../../common/detection_engine/rule_monitoring';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import type { IconColor } from '@elastic/eui';
|
||||
import { capitalize } from 'lodash';
|
||||
import { assertUnreachable } from '../../../../../common/utility_types';
|
||||
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common';
|
||||
import { RuleExecutionStatus } from '../../../../../common/detection_engine/rule_monitoring';
|
||||
|
||||
export const getStatusText = (value: RuleExecutionStatus | null | undefined): string | null => {
|
||||
if (value == null) {
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import type {
|
||||
GetAggregateRuleExecutionEventsResponse,
|
||||
GetInstalledIntegrationsResponse,
|
||||
RulesSchema,
|
||||
} from '../../../../../../common/detection_engine/schemas/response';
|
||||
|
@ -60,49 +59,6 @@ export const fetchRuleById = jest.fn(
|
|||
export const fetchRules = async (_: FetchRulesProps): Promise<FetchRulesResponse> =>
|
||||
Promise.resolve(rulesMock);
|
||||
|
||||
export const fetchRuleExecutionEvents = async ({
|
||||
ruleId,
|
||||
start,
|
||||
end,
|
||||
filters,
|
||||
signal,
|
||||
}: {
|
||||
ruleId: string;
|
||||
start: string;
|
||||
end: string;
|
||||
filters?: string;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<GetAggregateRuleExecutionEventsResponse> => {
|
||||
return Promise.resolve({
|
||||
events: [
|
||||
{
|
||||
duration_ms: 3866,
|
||||
es_search_duration_ms: 1236,
|
||||
execution_uuid: '88d15095-7937-462c-8f21-9763e1387cad',
|
||||
gap_duration_s: 0,
|
||||
indexing_duration_ms: 95,
|
||||
message:
|
||||
"rule executed: siem.queryRule:fb1fc150-a292-11ec-a2cf-c1b28b0392b0: 'Lots of Execution Events'",
|
||||
num_active_alerts: 0,
|
||||
num_errored_actions: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_succeeded_actions: 1,
|
||||
num_triggered_actions: 1,
|
||||
schedule_delay_ms: -127535,
|
||||
search_duration_ms: 1255,
|
||||
security_message: 'succeeded',
|
||||
security_status: 'succeeded',
|
||||
status: 'success',
|
||||
timed_out: false,
|
||||
timestamp: '2022-03-13T06:04:05.838Z',
|
||||
total_search_duration_ms: 0,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise<string[]> =>
|
||||
Promise.resolve(['elastic', 'love', 'quality', 'code']);
|
||||
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
import { KibanaServices } from '../../../../common/lib/kibana';
|
||||
|
||||
import {
|
||||
createRule,
|
||||
updateRule,
|
||||
|
@ -15,7 +17,6 @@ import {
|
|||
createPrepackagedRules,
|
||||
importRules,
|
||||
exportRules,
|
||||
fetchRuleExecutionEvents,
|
||||
fetchTags,
|
||||
getPrePackagedRulesStatus,
|
||||
previewRule,
|
||||
|
@ -27,7 +28,7 @@ import {
|
|||
} from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock';
|
||||
import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock';
|
||||
import { rulesMock } from './mock';
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
|
||||
const abortCtrl = new AbortController();
|
||||
const mockKibanaServices = KibanaServices.get as jest.Mock;
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
@ -617,56 +618,6 @@ describe('Detections Rules API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('fetchRuleExecutionEvents', () => {
|
||||
const responseMock = { events: [] };
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue(responseMock);
|
||||
});
|
||||
|
||||
test('calls API with correct parameters', async () => {
|
||||
await fetchRuleExecutionEvents({
|
||||
ruleId: '42',
|
||||
start: '2001-01-01T17:00:00.000Z',
|
||||
end: '2001-01-02T17:00:00.000Z',
|
||||
queryText: '',
|
||||
statusFilters: [],
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/internal/detection_engine/rules/42/execution/events',
|
||||
{
|
||||
method: 'GET',
|
||||
query: {
|
||||
end: '2001-01-02T17:00:00.000Z',
|
||||
page: undefined,
|
||||
per_page: undefined,
|
||||
query_text: '',
|
||||
sort_field: undefined,
|
||||
sort_order: undefined,
|
||||
start: '2001-01-01T17:00:00.000Z',
|
||||
status_filters: '',
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('returns API response as is', async () => {
|
||||
const response = await fetchRuleExecutionEvents({
|
||||
ruleId: '42',
|
||||
start: 'now-30',
|
||||
end: 'now',
|
||||
queryText: '',
|
||||
statusFilters: [],
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(response).toEqual(responseMock);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchTags', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { camelCase } from 'lodash';
|
||||
import dateMath from '@kbn/datemath';
|
||||
import type { HttpStart } from '@kbn/core/public';
|
||||
|
||||
import {
|
||||
|
@ -17,23 +15,17 @@ import {
|
|||
DETECTION_ENGINE_TAGS_URL,
|
||||
DETECTION_ENGINE_RULES_BULK_ACTION,
|
||||
DETECTION_ENGINE_RULES_PREVIEW,
|
||||
detectionEngineRuleExecutionEventsUrl,
|
||||
DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL,
|
||||
} from '../../../../../common/constants';
|
||||
import type {
|
||||
AggregateRuleExecutionEvent,
|
||||
BulkAction,
|
||||
RuleExecutionStatus,
|
||||
} from '../../../../../common/detection_engine/schemas/common';
|
||||
import type { BulkAction } from '../../../../../common/detection_engine/schemas/common';
|
||||
import type {
|
||||
FullResponseSchema,
|
||||
PreviewResponse,
|
||||
} from '../../../../../common/detection_engine/schemas/request';
|
||||
import type {
|
||||
RulesSchema,
|
||||
GetAggregateRuleExecutionEventsResponse,
|
||||
GetInstalledIntegrationsResponse,
|
||||
} from '../../../../../common/detection_engine/schemas/response';
|
||||
import type { GetInstalledIntegrationsResponse } from '../../../../../common/detection_engine/schemas/response/get_installed_integrations_response_schema';
|
||||
|
||||
import type {
|
||||
UpdateRulesProps,
|
||||
|
@ -321,64 +313,6 @@ export const exportRules = async ({
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch rule execution events (e.g. status changes) from Event Log.
|
||||
*
|
||||
* @param ruleId Saved Object ID of the rule (`rule.id`, not static `rule.rule_id`)
|
||||
* @param start Start daterange either in UTC ISO8601 or as datemath string (e.g. `2021-12-29T02:44:41.653Z` or `now-30`)
|
||||
* @param end End daterange either in UTC ISO8601 or as datemath string (e.g. `2021-12-29T02:44:41.653Z` or `now/w`)
|
||||
* @param queryText search string in querystring format (e.g. `event.duration > 1000 OR kibana.alert.rule.execution.metrics.execution_gap_duration_s > 100`)
|
||||
* @param statusFilters RuleExecutionStatus[] array of `statusFilters` (e.g. `succeeded,failed,partial failure`)
|
||||
* @param page current page to fetch
|
||||
* @param perPage number of results to fetch per page
|
||||
* @param sortField keyof AggregateRuleExecutionEvent field to sort by
|
||||
* @param sortOrder SortOrder what order to sort by (e.g. `asc` or `desc`)
|
||||
* @param signal AbortSignal Optional signal for cancelling the request
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
*/
|
||||
export const fetchRuleExecutionEvents = async ({
|
||||
ruleId,
|
||||
start,
|
||||
end,
|
||||
queryText,
|
||||
statusFilters,
|
||||
page,
|
||||
perPage,
|
||||
sortField,
|
||||
sortOrder,
|
||||
signal,
|
||||
}: {
|
||||
ruleId: string;
|
||||
start: string;
|
||||
end: string;
|
||||
queryText?: string;
|
||||
statusFilters?: RuleExecutionStatus[];
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
sortField?: keyof AggregateRuleExecutionEvent;
|
||||
sortOrder?: SortOrder;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<GetAggregateRuleExecutionEventsResponse> => {
|
||||
const url = detectionEngineRuleExecutionEventsUrl(ruleId);
|
||||
const startDate = dateMath.parse(start);
|
||||
const endDate = dateMath.parse(end, { roundUp: true });
|
||||
return KibanaServices.get().http.fetch<GetAggregateRuleExecutionEventsResponse>(url, {
|
||||
method: 'GET',
|
||||
query: {
|
||||
start: startDate?.utc().toISOString(),
|
||||
end: endDate?.utc().toISOString(),
|
||||
query_text: queryText,
|
||||
status_filters: statusFilters?.sort()?.join(','),
|
||||
page,
|
||||
per_page: perPage,
|
||||
sort_field: sortField,
|
||||
sort_order: sortOrder,
|
||||
},
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch all unique Tags used by Rules
|
||||
*
|
||||
|
|
|
@ -11,4 +11,3 @@ export * from './use_create_rule';
|
|||
export * from './types';
|
||||
export * from './use_rule';
|
||||
export * from './use_pre_packaged_rules';
|
||||
export * from './use_rule_execution_events';
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { GetAggregateRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response';
|
||||
import type { FetchRulesResponse, Rule } from './types';
|
||||
|
||||
export const savedRuleMock: Rule = {
|
||||
|
@ -136,170 +135,3 @@ export const rulesMock: FetchRulesResponse = {
|
|||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const ruleExecutionEventsMock: GetAggregateRuleExecutionEventsResponse = {
|
||||
events: [
|
||||
{
|
||||
execution_uuid: 'dc45a63c-4872-4964-a2d0-bddd8b2e634d',
|
||||
timestamp: '2022-04-28T21:19:08.047Z',
|
||||
duration_ms: 3,
|
||||
status: 'failure',
|
||||
message: 'siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: execution failed',
|
||||
num_active_alerts: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 0,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 0,
|
||||
total_search_duration_ms: 0,
|
||||
es_search_duration_ms: 0,
|
||||
schedule_delay_ms: 2169,
|
||||
timed_out: false,
|
||||
indexing_duration_ms: 0,
|
||||
search_duration_ms: 0,
|
||||
gap_duration_s: 0,
|
||||
security_status: 'failed',
|
||||
security_message: 'Rule failed to execute because rule ran after it was disabled.',
|
||||
},
|
||||
{
|
||||
execution_uuid: '0fde9271-05d0-4bfb-8ff8-815756d28350',
|
||||
timestamp: '2022-04-28T21:19:04.973Z',
|
||||
duration_ms: 1446,
|
||||
status: 'success',
|
||||
message:
|
||||
"rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'",
|
||||
num_active_alerts: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 0,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 0,
|
||||
total_search_duration_ms: 0,
|
||||
es_search_duration_ms: 0,
|
||||
schedule_delay_ms: 2089,
|
||||
timed_out: false,
|
||||
indexing_duration_ms: 0,
|
||||
search_duration_ms: 2,
|
||||
gap_duration_s: 0,
|
||||
security_status: 'succeeded',
|
||||
security_message: 'succeeded',
|
||||
},
|
||||
{
|
||||
execution_uuid: '5daaa259-ded8-4a52-853e-1e7652d325d5',
|
||||
timestamp: '2022-04-28T21:19:01.976Z',
|
||||
duration_ms: 1395,
|
||||
status: 'success',
|
||||
message:
|
||||
"rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'",
|
||||
num_active_alerts: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 0,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 0,
|
||||
total_search_duration_ms: 0,
|
||||
es_search_duration_ms: 1,
|
||||
schedule_delay_ms: 2637,
|
||||
timed_out: false,
|
||||
indexing_duration_ms: 0,
|
||||
search_duration_ms: 3,
|
||||
gap_duration_s: 0,
|
||||
security_status: 'succeeded',
|
||||
security_message: 'succeeded',
|
||||
},
|
||||
{
|
||||
execution_uuid: 'c7223e1c-4264-4a27-8697-0d720243fafc',
|
||||
timestamp: '2022-04-28T21:18:58.431Z',
|
||||
duration_ms: 1815,
|
||||
status: 'success',
|
||||
message:
|
||||
"rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'",
|
||||
num_active_alerts: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 0,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 0,
|
||||
total_search_duration_ms: 0,
|
||||
es_search_duration_ms: 1,
|
||||
schedule_delay_ms: -255429,
|
||||
timed_out: false,
|
||||
indexing_duration_ms: 0,
|
||||
search_duration_ms: 3,
|
||||
gap_duration_s: 0,
|
||||
security_status: 'succeeded',
|
||||
security_message: 'succeeded',
|
||||
},
|
||||
{
|
||||
execution_uuid: '1f6ba0c1-cc36-4f45-b919-7790b8a8d670',
|
||||
timestamp: '2022-04-28T21:18:13.954Z',
|
||||
duration_ms: 2055,
|
||||
status: 'success',
|
||||
message:
|
||||
"rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'",
|
||||
num_active_alerts: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 0,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 0,
|
||||
total_search_duration_ms: 0,
|
||||
es_search_duration_ms: 0,
|
||||
schedule_delay_ms: 2027,
|
||||
timed_out: false,
|
||||
indexing_duration_ms: 0,
|
||||
search_duration_ms: 0,
|
||||
gap_duration_s: 0,
|
||||
security_status: 'partial failure',
|
||||
security_message:
|
||||
'Check privileges failed to execute ResponseError: index_not_found_exception: [index_not_found_exception] Reason: no such index [yup] name: "Click here for hot fresh alerts!" id: "a6e61cf0-c737-11ec-9e32-e14913ffdd2d" rule id: "34946b12-88d1-49ef-82b7-9cad45972030" execution id: "1f6ba0c1-cc36-4f45-b919-7790b8a8d670" space ID: "default"',
|
||||
},
|
||||
{
|
||||
execution_uuid: 'b0f65d64-b229-432b-9d39-f4385a7f9368',
|
||||
timestamp: '2022-04-28T21:15:43.086Z',
|
||||
duration_ms: 1205,
|
||||
status: 'success',
|
||||
message:
|
||||
"rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'",
|
||||
num_active_alerts: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 0,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 0,
|
||||
total_search_duration_ms: 0,
|
||||
es_search_duration_ms: 672,
|
||||
schedule_delay_ms: 3086,
|
||||
timed_out: false,
|
||||
indexing_duration_ms: 140,
|
||||
search_duration_ms: 684,
|
||||
gap_duration_s: 0,
|
||||
security_status: 'succeeded',
|
||||
security_message: 'succeeded',
|
||||
},
|
||||
{
|
||||
execution_uuid: '7bfd25b9-c0d8-44b1-982c-485169466a8e',
|
||||
timestamp: '2022-04-28T21:10:40.135Z',
|
||||
duration_ms: 6321,
|
||||
status: 'success',
|
||||
message:
|
||||
"rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'",
|
||||
num_active_alerts: 0,
|
||||
num_new_alerts: 0,
|
||||
num_recovered_alerts: 0,
|
||||
num_triggered_actions: 0,
|
||||
num_succeeded_actions: 0,
|
||||
num_errored_actions: 0,
|
||||
total_search_duration_ms: 0,
|
||||
es_search_duration_ms: 930,
|
||||
schedule_delay_ms: 1222,
|
||||
timed_out: false,
|
||||
indexing_duration_ms: 2103,
|
||||
search_duration_ms: 946,
|
||||
gap_duration_s: 0,
|
||||
security_status: 'succeeded',
|
||||
security_message: 'succeeded',
|
||||
},
|
||||
],
|
||||
total: 7,
|
||||
};
|
||||
|
|
|
@ -109,10 +109,3 @@ export const RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES = (
|
|||
'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} and {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ',
|
||||
}
|
||||
);
|
||||
|
||||
export const RULE_EXECUTION_EVENTS_FETCH_FAILURE = i18n.translate(
|
||||
'xpack.securitySolution.containers.detectionEngine.ruleExecutionEventsFetchFailDescription',
|
||||
{
|
||||
defaultMessage: 'Failed to fetch rule execution events',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -22,6 +22,9 @@ import {
|
|||
severity_mapping,
|
||||
severity,
|
||||
} from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
|
||||
import { RuleExecutionSummary } from '../../../../../common/detection_engine/rule_monitoring';
|
||||
|
||||
import type {
|
||||
SortOrder,
|
||||
BulkAction,
|
||||
|
@ -41,7 +44,6 @@ import {
|
|||
event_category_override,
|
||||
tiebreaker_field,
|
||||
threshold,
|
||||
ruleExecutionSummary,
|
||||
RelatedIntegrationArray,
|
||||
RequiredFieldArray,
|
||||
SetupGuide,
|
||||
|
@ -169,7 +171,7 @@ export const RuleSchema = t.intersection([
|
|||
exceptions_list: listArray,
|
||||
uuid: t.string,
|
||||
version: t.number,
|
||||
execution_summary: ruleExecutionSummary,
|
||||
execution_summary: RuleExecutionSummary,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { useQuery } from 'react-query';
|
||||
import type {
|
||||
AggregateRuleExecutionEvent,
|
||||
RuleExecutionStatus,
|
||||
} from '../../../../../common/detection_engine/schemas/common';
|
||||
import type { GetAggregateRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import { fetchRuleExecutionEvents } from './api';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface UseRuleExecutionEventsArgs {
|
||||
ruleId: string;
|
||||
start: string;
|
||||
end: string;
|
||||
queryText?: string;
|
||||
statusFilters?: RuleExecutionStatus[];
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
sortField?: keyof AggregateRuleExecutionEvent;
|
||||
sortOrder?: SortOrder;
|
||||
}
|
||||
|
||||
export const useRuleExecutionEvents = ({
|
||||
ruleId,
|
||||
start,
|
||||
end,
|
||||
queryText,
|
||||
statusFilters,
|
||||
page,
|
||||
perPage,
|
||||
sortField,
|
||||
sortOrder,
|
||||
}: UseRuleExecutionEventsArgs) => {
|
||||
const { addError } = useAppToasts();
|
||||
|
||||
return useQuery<GetAggregateRuleExecutionEventsResponse>(
|
||||
[
|
||||
'ruleExecutionEvents',
|
||||
{
|
||||
ruleId,
|
||||
start,
|
||||
end,
|
||||
queryText,
|
||||
statusFilters,
|
||||
page,
|
||||
perPage,
|
||||
sortField,
|
||||
sortOrder,
|
||||
},
|
||||
],
|
||||
async ({ signal }) => {
|
||||
return fetchRuleExecutionEvents({
|
||||
ruleId,
|
||||
start,
|
||||
end,
|
||||
queryText,
|
||||
statusFilters,
|
||||
page,
|
||||
perPage,
|
||||
sortField,
|
||||
sortOrder,
|
||||
signal,
|
||||
});
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
onError: (e) => {
|
||||
addError(e, { title: i18n.RULE_EXECUTION_EVENTS_FETCH_FAILURE });
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
|
@ -39,7 +39,7 @@ import { RuleStatusBadge } from '../../../../components/rules/rule_execution_sta
|
|||
import type {
|
||||
DurationMetric,
|
||||
RuleExecutionSummary,
|
||||
} from '../../../../../../common/detection_engine/schemas/common';
|
||||
} from '../../../../../../common/detection_engine/rule_monitoring';
|
||||
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
|
||||
import { useStartTransaction } from '../../../../../common/lib/apm/use_start_transaction';
|
||||
import { useInvalidateRules } from '../../../../containers/detection_engine/rules/use_find_rules_query';
|
||||
|
|
|
@ -10,7 +10,7 @@ import type { RuleDetailsContextType } from '../rule_details_context';
|
|||
|
||||
export const useRuleDetailsContextMock = {
|
||||
create: (): jest.Mocked<RuleDetailsContextType> => ({
|
||||
executionLogs: {
|
||||
executionResults: {
|
||||
state: {
|
||||
superDatePicker: {
|
||||
recentlyUsedRanges: [],
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue