Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Handle specific fields in /upgrade/_review endpoint and refactor diff logic to use Zod #186615

Merged
merged 32 commits into from
Jul 11, 2024
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7691684
Omit props from prebuilt rule asset schema
jpdjere Jun 21, 2024
18bcc57
Removed fields from RuleAsset type
jpdjere Jun 26, 2024
85c281a
draft changes
jpdjere Jun 26, 2024
ecab338
Cleanup
jpdjere Jun 26, 2024
62b26e5
More
jpdjere Jun 26, 2024
0d8e714
Clean up rule_schema_legacy
jpdjere Jun 27, 2024
9bde429
Introduced deprecation warning
jpdjere Jun 27, 2024
bbb68b8
generated schemas
jpdjere Jun 27, 2024
4cd1a9c
Fix
jpdjere Jun 27, 2024
1448c6e
Fix
jpdjere Jun 27, 2024
576c940
Added comment
jpdjere Jun 27, 2024
648f9b1
Fixes PrebuiltRuleAsset test
jpdjere Jun 27, 2024
411aa38
Add comment
jpdjere Jun 27, 2024
cad1ba9
Fix type
jpdjere Jun 28, 2024
af19890
Type fixes
jpdjere Jun 28, 2024
e9b639b
Fix one more type
jpdjere Jun 28, 2024
3ec4033
Fix
jpdjere Jun 28, 2024
c59d7f7
Fix
jpdjere Jun 28, 2024
16afe99
Reintroduced alert_suppression from PrebuiltRuleAsset schema
jpdjere Jun 28, 2024
b1c1bb8
Fix test
jpdjere Jun 28, 2024
4bc346c
Adjustments made during code review
banderror Jul 4, 2024
979c202
Fix export
banderror Jul 4, 2024
49d8866
Add description to schema
jpdjere Jul 5, 2024
e16364d
Remove checks
jpdjere Jul 5, 2024
4d248f8
Add describe block
jpdjere Jul 5, 2024
0b49b9b
Reworked prebuilt rule asset
jpdjere Jul 5, 2024
865b353
Fix import
jpdjere Jul 5, 2024
d57dd65
Add back Time Duration tests
jpdjere Jul 5, 2024
4143a1f
Add license header
jpdjere Jul 5, 2024
2be9999
Delete check
jpdjere Jul 5, 2024
ec0d180
Lint/types fixes
jpdjere Jul 5, 2024
9fefee6
Fix test
jpdjere Jul 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -304,8 +304,29 @@ export type TimestampOverrideFallbackDisabled = z.infer<typeof TimestampOverride
export const TimestampOverrideFallbackDisabled = z.boolean();

/**
* Describes an Elasticsearch field that is needed for the rule to function
*/
* Describes an Elasticsearch field that is needed for the rule to function.

Almost all types of Security rules check source event documents for a match to some kind of
query or filter. If a document has certain field with certain values, then it's a match and
the rule will generate an alert.

Required field is an event field that must be present in the source indices of a given rule.

@example
const standardEcsField: RequiredField = {
name: 'event.action',
type: 'keyword',
ecs: true,
};

@example
const nonEcsField: RequiredField = {
name: 'winlog.event_data.AttributeLDAPDisplayName',
type: 'keyword',
ecs: false,
};

*/
export type RequiredField = z.infer<typeof RequiredField>;
export const RequiredField = z.object({
/**
@@ -368,6 +389,39 @@ export const SavedObjectResolveAliasPurpose = z.enum([
export type SavedObjectResolveAliasPurposeEnum = typeof SavedObjectResolveAliasPurpose.enum;
export const SavedObjectResolveAliasPurposeEnum = SavedObjectResolveAliasPurpose.enum;

/**
* Related integration is a potential dependency of a rule. It's assumed that if the user installs
one of the related integrations of a rule, the rule might start to work properly because it will
have source events (generated by this integration) potentially matching the rule's query.

NOTE: Proper work is not guaranteed, because a related integration, if installed, can be
configured differently or generate data that is not necessarily relevant for this rule.

Related integration is a combination of a Fleet package and (optionally) one of the
package's "integrations" that this package contains. It is represented by 3 properties:

- `package`: name of the package (required, unique id)
- `version`: version of the package (required, semver-compatible)
- `integration`: name of the integration of this package (optional, id within the package)

There are Fleet packages like `windows` that contain only one integration; in this case,
`integration` should be unspecified. There are also packages like `aws` and `azure` that contain
several integrations; in this case, `integration` should be specified.

@example
const x: RelatedIntegration = {
package: 'windows',
version: '1.5.x',
};

@example
const x: RelatedIntegration = {
package: 'azure',
version: '~1.1.6',
integration: 'activitylogs',
};

*/
export type RelatedIntegration = z.infer<typeof RelatedIntegration>;
export const RelatedIntegration = z.object({
package: NonEmptyString,
@@ -378,6 +432,22 @@ export const RelatedIntegration = z.object({
export type RelatedIntegrationArray = z.infer<typeof RelatedIntegrationArray>;
export const RelatedIntegrationArray = z.array(RelatedIntegration);

/**
* Schema for fields relating to investigation fields. These are user defined fields we use to highlight
in various features in the UI such as alert details flyout and exceptions auto-population from alert.
Added in PR #163235
Right now we only have a single field but anticipate adding more related fields to store various
configuration states such as `override` - where a user might say if they want only these fields to
display, or if they want these fields + the fields we select. When expanding this field, it may look
something like:
```typescript
const investigationFields = z.object({
field_names: NonEmptyArray(NonEmptyString),
override: z.boolean().optional(),
});
```

*/
export type InvestigationFields = z.infer<typeof InvestigationFields>;
export const InvestigationFields = z.object({
field_names: z.array(NonEmptyString).min(1),
Original file line number Diff line number Diff line change
@@ -315,7 +315,28 @@ components:

RequiredField:
type: object
description: Describes an Elasticsearch field that is needed for the rule to function
description: |
Describes an Elasticsearch field that is needed for the rule to function.

Almost all types of Security rules check source event documents for a match to some kind of
query or filter. If a document has certain field with certain values, then it's a match and
the rule will generate an alert.

Required field is an event field that must be present in the source indices of a given rule.

@example
const standardEcsField: RequiredField = {
name: 'event.action',
type: 'keyword',
ecs: true,
};

@example
const nonEcsField: RequiredField = {
name: 'winlog.event_data.AttributeLDAPDisplayName',
type: 'keyword',
ecs: false,
};
properties:
name:
$ref: '../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString'
@@ -376,6 +397,37 @@ components:

RelatedIntegration:
type: object
description: |
Related integration is a potential dependency of a rule. It's assumed that if the user installs
one of the related integrations of a rule, the rule might start to work properly because it will
have source events (generated by this integration) potentially matching the rule's query.

NOTE: Proper work is not guaranteed, because a related integration, if installed, can be
configured differently or generate data that is not necessarily relevant for this rule.

Related integration is a combination of a Fleet package and (optionally) one of the
package's "integrations" that this package contains. It is represented by 3 properties:

- `package`: name of the package (required, unique id)
- `version`: version of the package (required, semver-compatible)
- `integration`: name of the integration of this package (optional, id within the package)

There are Fleet packages like `windows` that contain only one integration; in this case,
`integration` should be unspecified. There are also packages like `aws` and `azure` that contain
several integrations; in this case, `integration` should be specified.

@example
const x: RelatedIntegration = {
package: 'windows',
version: '1.5.x',
};

@example
const x: RelatedIntegration = {
package: 'azure',
version: '~1.1.6',
integration: 'activitylogs',
};
properties:
package:
$ref: '../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString'
@@ -392,10 +444,22 @@ components:
items:
$ref: '#/components/schemas/RelatedIntegration'

# Schema for fields relating to investigation fields, these are user defined fields we use to highlight in various features in the UI such as alert details flyout and exceptions auto-population from alert. Added in PR #163235
# Right now we only have a single field but anticipate adding more related fields to store various configuration states such as `override` - where a user might say if they want only these fields to display, or if they want these fields + the fields we select.
InvestigationFields:
type: object
description: |
Schema for fields relating to investigation fields. These are user defined fields we use to highlight
in various features in the UI such as alert details flyout and exceptions auto-population from alert.
Added in PR #163235
Right now we only have a single field but anticipate adding more related fields to store various
configuration states such as `override` - where a user might say if they want only these fields to
display, or if they want these fields + the fields we select. When expanding this field, it may look
something like:
```typescript
const investigationFields = z.object({
field_names: NonEmptyArray(NonEmptyString),
override: z.boolean().optional(),
});
```
properties:
field_names:
type: array
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* 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 { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import { TimeDuration } from './time_duration'; // Update with the actual path to your TimeDuration file

describe('TimeDuration schema', () => {
test('it should validate a correctly formed TimeDuration with time unit of seconds', () => {
const payload = '1s';
const schema = TimeDuration({ allowedUnits: ['s'] });

const result = schema.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});

test('it should validate a correctly formed TimeDuration with time unit of minutes', () => {
const payload = '100m';
const schema = TimeDuration({ allowedUnits: ['s', 'm'] });

const result = schema.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});

test('it should validate a correctly formed TimeDuration with time unit of hours', () => {
const payload = '10000000h';
const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h'] });

const result = schema.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});

test('it should validate a correctly formed TimeDuration with time unit of days', () => {
const payload = '7d';
const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] });

const result = schema.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});

test('it should NOT validate a correctly formed TimeDuration with time unit of seconds if it is not an allowed unit', () => {
const payload = '30s';
const schema = TimeDuration({ allowedUnits: ['m', 'h', 'd'] });

const result = schema.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"Invalid time duration format. Must be 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\\", \\"7d\\""`
);
});

test('it should NOT validate a negative TimeDuration', () => {
const payload = '-10s';
const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] });

const result = schema.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"Invalid time duration format. Must be 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\\", \\"7d\\""`
);
});

test('it should NOT validate a fractional number', () => {
const payload = '1.5s';
const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] });

const result = schema.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"Invalid time duration format. Must be 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\\", \\"7d\\""`
);
});

test('it should NOT validate a TimeDuration with an invalid time unit', () => {
const payload = '10000000days';
const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h', 'd'] });

const result = schema.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"Invalid time duration format. Must be 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\\", \\"7d\\""`
);
});

test('it should NOT validate a TimeDuration with a time interval with incorrect format', () => {
const payload = '100ff0000w';
const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h'] });

const result = schema.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"Invalid time duration format. Must be 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\\", \\"7d\\""`
);
});

test('it should NOT validate an empty string', () => {
const payload = '';
const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h'] });

const result = schema.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"Invalid time duration format. Must be 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\\", \\"7d\\""`
);
});

test('it should NOT validate a number', () => {
const payload = 100;
const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h'] });

const result = schema.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"Expected string, received number"`
);
});

test('it should NOT validate a TimeDuration with a valid time unit but unsafe integer', () => {
const payload = `${Math.pow(2, 53)}h`;
const schema = TimeDuration({ allowedUnits: ['s', 'm', 'h'] });

const result = schema.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"Invalid time duration format. Must be 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\\", \\"7d\\""`
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { z } from 'zod';

type TimeUnits = 's' | 'm' | 'h' | 'd' | 'w' | 'y';

interface TimeDurationType {
allowedUnits: TimeUnits[];
}

const isTimeSafe = (time: number) => time >= 1 && Number.isSafeInteger(time);

/**
* 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", "7d"
*
* Example usage:
* ```
* const schedule: RuleSchedule = {
* interval: TimeDuration({ allowedUnits: ['s', 'm', 'h'] }).parse('3h'),
* };
* ```
*/
export const TimeDuration = ({ allowedUnits }: TimeDurationType) => {
return z.string().refine(
(input) => {
if (input.trim() === '') return false;

try {
const inputLength = input.length;
const time = Number(input.trim().substring(0, inputLength - 1));
const unit = input.trim().at(-1) as TimeUnits;

return isTimeSafe(time) && allowedUnits.includes(unit);
} catch (error) {
return false;
}
},
{
message:
'Invalid time duration format. Must be 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", "7d"',
}
);
};

export type TimeDurationSchema = ReturnType<typeof TimeDuration>;
export type TimeDuration = z.infer<TimeDurationSchema>;
Loading