-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Security Solution] Handle specific fields in
/upgrade/_review
endp…
…oint and refactor diff logic to use Zod (#186615) Fixes: #180393 ## Summary Handles specific fields in `/upgrade/_review` endpoint upgrade workflow, as described in #180393. Achieves this with two mechanisms: 1. Removing fields from the `PrebuiltRuleAsset` schema, which excludes the field from the diff calculation completely. 2. Manually removing the diff calculation for certain fields, by excluding them from `/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.ts` Also, refactors a part of the codebase from its prior usage of `io-ts` schema types to use autogenerated Zod types. With this refactor, most of the `x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema_legacy` could be deleted. Unluckily some of the types manually created there are still used in some complex types elsewhere, so I added a note to that file indicating that those should be migrated to Zod, so that the legacy folder can finally be deleted. ### Checklist Delete any items that are not applicable to this PR. - [ ] [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 - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed ### For maintainers - [ ] 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) --------- Co-authored-by: Georgii Gorbachev <[email protected]>
- Loading branch information
Showing
27 changed files
with
730 additions
and
1,409 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
135 changes: 135 additions & 0 deletions
135
...ins/security_solution/common/api/detection_engine/model/rule_schema/time_duration.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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\\""` | ||
); | ||
}); | ||
}); |
53 changes: 53 additions & 0 deletions
53
.../plugins/security_solution/common/api/detection_engine/model/rule_schema/time_duration.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |
Oops, something went wrong.