Skip to content

Commit

Permalink
[Security Solution][Detections] Add Bulk Scheduling for rules (#140166)
Browse files Browse the repository at this point in the history
Addresses [#2127](https://github.com/elastic/security-team/issues/2172) (internal)

## Summary

Adds feature to bulk edit schedule of rules (interval -runs every- and lookback time)


https://user-images.githubusercontent.com/5354282/188846852-8bcb128a-db02-4a81-9fc8-3029a97965c2.mov


### 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)
- [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials
- [ ] [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
- [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] 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] 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))
  • Loading branch information
jpdjere authored Sep 19, 2022
1 parent 167587f commit 672bdd2
Show file tree
Hide file tree
Showing 24 changed files with 763 additions and 15 deletions.
1 change: 1 addition & 0 deletions packages/kbn-securitysolution-io-ts-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ export * from './src/operator';
export * from './src/positive_integer_greater_than_zero';
export * from './src/positive_integer';
export * from './src/string_to_positive_number';
export * from './src/time_duration';
export * from './src/uuid';
export * from './src/version';
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 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 { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { TimeDuration } from '.';
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';

describe('time_unit', () => {
test('it should validate a correctly formed TimeDuration with time unit of seconds', () => {
const payload = '1s';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});

test('it should validate a correctly formed TimeDuration with time unit of minutes', () => {
const payload = '100m';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});

test('it should validate a correctly formed TimeDuration with time unit of hours', () => {
const payload = '10000000h';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});

test('it should NOT validate a negative TimeDuration', () => {
const payload = '-10s';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([
'Invalid value "-10s" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});

test('it should NOT validate a TimeDuration with some other time unit', () => {
const payload = '10000000w';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([
'Invalid value "10000000w" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});

test('it should NOT validate a TimeDuration with a time interval with incorrect format', () => {
const payload = '100ff0000w';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([
'Invalid value "100ff0000w" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});

test('it should NOT validate an empty string', () => {
const payload = '';
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "TimeDuration"']);
expect(message.schema).toEqual({});
});

test('it should NOT validate an number', () => {
const payload = 100;
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([
'Invalid value "100" supplied to "TimeDuration"',
]);
expect(message.schema).toEqual({});
});

test('it should NOT validate an TimeDuration with a valid time unit but unsafe integer', () => {
const payload = `${Math.pow(2, 53)}h`;
const decoded = TimeDuration.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([
`Invalid value "${Math.pow(2, 53)}h" supplied to "TimeDuration"`,
]);
expect(message.schema).toEqual({});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 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';

/**
* Types the TimeDuration as:
* - A string that is not empty, and composed of a positive integer greater than 0 followed by a unit of time
* - in the format {safe_integer}{timeUnit}, e.g. "30s", "1m", "2h"
*/
export const TimeDuration = new t.Type<string, string, unknown>(
'TimeDuration',
t.string.is,
(input, context): Either<t.Errors, string> => {
if (typeof input === 'string' && input.trim() !== '') {
try {
const inputLength = input.length;
const time = parseInt(input.trim().substring(0, inputLength - 1), 10);
const unit = input.trim().at(-1);
if (
time >= 1 &&
Number.isSafeInteger(time) &&
(unit === 's' || unit === 'm' || unit === 'h')
) {
return t.success(input);
} else {
return t.failure(input, context);
}
} catch (error) {
return t.failure(input, context);
}
} else {
return t.failure(input, context);
}
},
t.identity
);

export type TimeDurationC = typeof TimeDuration;
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,125 @@ describe('perform_bulk_action_schema', () => {
});
});

describe('schedules', () => {
test('invalid request: wrong schedules payload type', () => {
const payload = {
query: 'name: test',
action: BulkAction.edit,
[BulkAction.edit]: [{ type: BulkActionEditType.set_schedule, value: [] }],
};

const message = retrieveValidationMessage(payload);

expect(getPaths(left(message.errors))).toEqual([
'Invalid value "edit" supplied to "action"',
'Invalid value "set_schedule" supplied to "edit,type"',
'Invalid value "[]" supplied to "edit,value"',
]);
expect(message.schema).toEqual({});
});

test('invalid request: wrong type of payload data', () => {
const payload = {
query: 'name: test',
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.set_schedule,
value: {
interval: '-10m',
lookback: '1m',
},
},
],
} as PerformBulkActionSchema;

const message = retrieveValidationMessage(payload);

expect(getPaths(left(message.errors))).toEqual(
expect.arrayContaining([
'Invalid value "edit" supplied to "action"',
'Invalid value "{"interval":"-10m","lookback":"1m"}" supplied to "edit,value"',
'Invalid value "-10m" supplied to "edit,value,interval"',
])
);
expect(message.schema).toEqual({});
});

test('invalid request: missing interval', () => {
const payload = {
query: 'name: test',
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.set_schedule,
value: {
lookback: '1m',
},
},
],
} as PerformBulkActionSchema;

const message = retrieveValidationMessage(payload);

expect(getPaths(left(message.errors))).toEqual(
expect.arrayContaining([
'Invalid value "edit" supplied to "action"',
'Invalid value "{"lookback":"1m"}" supplied to "edit,value"',
'Invalid value "undefined" supplied to "edit,value,interval"',
])
);
expect(message.schema).toEqual({});
});

test('invalid request: missing lookback', () => {
const payload = {
query: 'name: test',
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.set_schedule,
value: {
interval: '1m',
},
},
],
} as PerformBulkActionSchema;

const message = retrieveValidationMessage(payload);

expect(getPaths(left(message.errors))).toEqual(
expect.arrayContaining([
'Invalid value "edit" supplied to "action"',
'Invalid value "{"interval":"1m"}" supplied to "edit,value"',
'Invalid value "undefined" supplied to "edit,value,lookback"',
])
);
expect(message.schema).toEqual({});
});

test('valid request: set_schedule edit action', () => {
const payload: PerformBulkActionSchema = {
query: 'name: test',
action: BulkAction.edit,
[BulkAction.edit]: [
{
type: BulkActionEditType.set_schedule,
value: {
interval: '1m',
lookback: '1m',
},
},
],
} as PerformBulkActionSchema;

const message = retrieveValidationMessage(payload);

expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});
});

describe('rule actions', () => {
test('invalid request: invalid rule actions payload', () => {
const payload = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import * as t from 'io-ts';
import { NonEmptyArray, enumeration } from '@kbn/securitysolution-io-ts-types';
import { NonEmptyArray, TimeDuration, enumeration } from '@kbn/securitysolution-io-ts-types';

import {
throttle,
Expand Down Expand Up @@ -38,6 +38,7 @@ export enum BulkActionEditType {
'set_timeline' = 'set_timeline',
'add_rule_actions' = 'add_rule_actions',
'set_rule_actions' = 'set_rule_actions',
'set_schedule' = 'set_schedule',
}

const bulkActionEditPayloadTags = t.type({
Expand Down Expand Up @@ -102,11 +103,21 @@ const bulkActionEditPayloadRuleActions = t.type({

export type BulkActionEditPayloadRuleActions = t.TypeOf<typeof bulkActionEditPayloadRuleActions>;

const bulkActionEditPayloadSchedule = t.type({
type: t.literal(BulkActionEditType.set_schedule),
value: t.type({
interval: TimeDuration,
lookback: TimeDuration,
}),
});
export type BulkActionEditPayloadSchedule = t.TypeOf<typeof bulkActionEditPayloadSchedule>;

export const bulkActionEditPayload = t.union([
bulkActionEditPayloadTags,
bulkActionEditPayloadIndexPatterns,
bulkActionEditPayloadTimeline,
bulkActionEditPayloadRuleActions,
bulkActionEditPayloadSchedule,
]);

export type BulkActionEditPayload = t.TypeOf<typeof bulkActionEditPayload>;
Expand All @@ -116,14 +127,16 @@ export type BulkActionEditPayload = t.TypeOf<typeof bulkActionEditPayload>;
*/
export type BulkActionEditForRuleAttributes =
| BulkActionEditPayloadTags
| BulkActionEditPayloadRuleActions;
| BulkActionEditPayloadRuleActions
| BulkActionEditPayloadSchedule;

/**
* actions that modify rules params
*/
export type BulkActionEditForRuleParams =
| BulkActionEditPayloadIndexPatterns
| BulkActionEditPayloadTimeline;
| BulkActionEditPayloadTimeline
| BulkActionEditPayloadSchedule;

export const performBulkActionSchema = t.intersection([
t.exact(
Expand Down
Loading

0 comments on commit 672bdd2

Please sign in to comment.