Skip to content

Commit

Permalink
[8.x] [Security Solution] Allow importing of prebuilt rules via the A…
Browse files Browse the repository at this point in the history
…PI (#190198) (#196613)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Security Solution] Allow importing of prebuilt rules via the API
(#190198)](#190198)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Ryland
Herrick","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-10-16T21:19:05Z","message":"[Security
Solution] Allow importing of prebuilt rules via the API (#190198)\n\n##
Summary\r\n\r\nThis PR introduces the backend functionality necessary to
import\r\nprebuilt rules via our existing import API.
The\r\n[RFC](https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/docs/rfcs/detection_response/prebuilt_rules_customization.md)\r\ngoes
into detail, and the import-specific issue is
described\r\n[here](#180168),
but at a high\r\nlevel we're adding two things in this PR:\r\n\r\n1. The
logic to calculate the new `rule_source` field on import,
which\r\ncontains the information about the rule's relationship to
existing\r\nprebuilt rules.\r\n2. A new code path for importing rules,
which respects the calculated\r\n`rule_source` field.\r\n\r\nIn order to
maintain backwards compatibility with the existing import\r\nlogic, and
because the existing import implementation is hard to\r\nmodify/extend,
I opted to add a top-level switch on the feature flag in\r\nthe import
route itself, which calls either the existing import function\r\n(now
named `importRulesLegacy`), or the new function, `importRules`,\r\nwhich
ultimately calls the new `DetectionRulesClient`
method,\r\n`importRules`. Neither knows about the feature flag, and
thanks to great\r\nsuggestions from @xcrzx there are nice, clean
boundaries between the\r\nimport functions and the client
methods.\r\n\r\nI went down the path of trying to write the new import
code by reusing\r\nthe outer `importRules` function, but after
discussion with the team we\r\ndecided it was simplest to simply
bifurcate the code at that point, so\r\nthat we have:\r\n\r\n1. The
legacy import code, which:\r\n * only supports custom rules (`immutable:
false`)\r\n * accepts `version` as an optional parameter\r\n* calculates
a legacy `rule_source` value based on the `immutable` field\r\n2. The
new import code, which\r\n * Installs the prebuilt rules assets as
necessary\r\n * Accepts both kinds of rules (`immutable:
true/false`)\r\n * Requires `version` as a parameter for _prebuilt_
rules\r\n * Allows `version` to be optional for custom rules\r\n *
calculates a proper `rule_source` (and `immutable`)\r\n\r\n\r\n###
Deprecation of `immutable`\r\nThe RFC (and thus I) had initially thought
that we'd be deprecating the\r\n`immutable` field as part of this
PR/Epic. However,
after\r\n[discussion](https://github.com/elastic/kibana/pull/190198#discussion_r1736021749)\r\nwe
have opted to continue supporting `immutable` until such time as
we\r\ncan drop it, compatibility-wise.\r\n\r\n\r\n## Steps to
Review\r\n1. Enable the Feature Flag:
`prebuiltRulesCustomizationEnabled`\r\n2. Install the prebuilt rules
package via fleet\r\n3. Create and export a custom rule to serve as a
template (or download\r\none:\r\n\r\ncurl -L -H 'Authorization:
8eef0fe5d7dfc52b' -o
'rules_export\r\n(1).ndjson'\r\nhttps://upload.elastic.co/d/4693e7fe4356ce7bcf7b7d6b72881a9fd189730c61bf5ef47c9930458d746979\r\n
)\r\n \r\n4. Install some prebuilt rules, and obtain a prebuilt rule's
`rule_id`,\r\ne.g. `ac8805f6-1e08-406c-962e-3937057fa86f`\r\n5. Edit the
`rules_export.ndjson` to contain only the first line, and\r\nmodify the
`rule_id` with the prebuilt rule's\r\n6. Import `rules_export.ndjson`
and then retrieve the rule via the
Dev\r\nConsole:\r\n\r\nGET\r\nkbn:api/detection_engine/rules?rule_id=ac8805f6-1e08-406c-962e-3937057fa86f\r\n\r\n7.
Observe that the rule has the expected `rule_source` and
`immutable`\r\nvalues\r\n8. Test other permutations of import by
modifying `rules_export.ndjson`\r\nand re-importing; see ([the
test\r\nplan](#191116) as well as
a\r\n[reference table
of\r\nscenarios](https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/docs/rfcs/detection_response/prebuilt_rules_customization.md#importing-rules))\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>\r\nCo-authored-by:
Georgii Gorbachev
<[email protected]>","sha":"c0b1301a21b74e8b643fc0e898b1d982fd85d98e","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:Detections
and Resp","Team: SecuritySolution","Team:Detection Rule
Management","Feature:Prebuilt Detection
Rules","v8.16.0","backport:version"],"title":"[Security Solution] Allow
importing of prebuilt rules via the
API","number":190198,"url":"https://github.com/elastic/kibana/pull/190198","mergeCommit":{"message":"[Security
Solution] Allow importing of prebuilt rules via the API (#190198)\n\n##
Summary\r\n\r\nThis PR introduces the backend functionality necessary to
import\r\nprebuilt rules via our existing import API.
The\r\n[RFC](https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/docs/rfcs/detection_response/prebuilt_rules_customization.md)\r\ngoes
into detail, and the import-specific issue is
described\r\n[here](#180168),
but at a high\r\nlevel we're adding two things in this PR:\r\n\r\n1. The
logic to calculate the new `rule_source` field on import,
which\r\ncontains the information about the rule's relationship to
existing\r\nprebuilt rules.\r\n2. A new code path for importing rules,
which respects the calculated\r\n`rule_source` field.\r\n\r\nIn order to
maintain backwards compatibility with the existing import\r\nlogic, and
because the existing import implementation is hard to\r\nmodify/extend,
I opted to add a top-level switch on the feature flag in\r\nthe import
route itself, which calls either the existing import function\r\n(now
named `importRulesLegacy`), or the new function, `importRules`,\r\nwhich
ultimately calls the new `DetectionRulesClient`
method,\r\n`importRules`. Neither knows about the feature flag, and
thanks to great\r\nsuggestions from @xcrzx there are nice, clean
boundaries between the\r\nimport functions and the client
methods.\r\n\r\nI went down the path of trying to write the new import
code by reusing\r\nthe outer `importRules` function, but after
discussion with the team we\r\ndecided it was simplest to simply
bifurcate the code at that point, so\r\nthat we have:\r\n\r\n1. The
legacy import code, which:\r\n * only supports custom rules (`immutable:
false`)\r\n * accepts `version` as an optional parameter\r\n* calculates
a legacy `rule_source` value based on the `immutable` field\r\n2. The
new import code, which\r\n * Installs the prebuilt rules assets as
necessary\r\n * Accepts both kinds of rules (`immutable:
true/false`)\r\n * Requires `version` as a parameter for _prebuilt_
rules\r\n * Allows `version` to be optional for custom rules\r\n *
calculates a proper `rule_source` (and `immutable`)\r\n\r\n\r\n###
Deprecation of `immutable`\r\nThe RFC (and thus I) had initially thought
that we'd be deprecating the\r\n`immutable` field as part of this
PR/Epic. However,
after\r\n[discussion](https://github.com/elastic/kibana/pull/190198#discussion_r1736021749)\r\nwe
have opted to continue supporting `immutable` until such time as
we\r\ncan drop it, compatibility-wise.\r\n\r\n\r\n## Steps to
Review\r\n1. Enable the Feature Flag:
`prebuiltRulesCustomizationEnabled`\r\n2. Install the prebuilt rules
package via fleet\r\n3. Create and export a custom rule to serve as a
template (or download\r\none:\r\n\r\ncurl -L -H 'Authorization:
8eef0fe5d7dfc52b' -o
'rules_export\r\n(1).ndjson'\r\nhttps://upload.elastic.co/d/4693e7fe4356ce7bcf7b7d6b72881a9fd189730c61bf5ef47c9930458d746979\r\n
)\r\n \r\n4. Install some prebuilt rules, and obtain a prebuilt rule's
`rule_id`,\r\ne.g. `ac8805f6-1e08-406c-962e-3937057fa86f`\r\n5. Edit the
`rules_export.ndjson` to contain only the first line, and\r\nmodify the
`rule_id` with the prebuilt rule's\r\n6. Import `rules_export.ndjson`
and then retrieve the rule via the
Dev\r\nConsole:\r\n\r\nGET\r\nkbn:api/detection_engine/rules?rule_id=ac8805f6-1e08-406c-962e-3937057fa86f\r\n\r\n7.
Observe that the rule has the expected `rule_source` and
`immutable`\r\nvalues\r\n8. Test other permutations of import by
modifying `rules_export.ndjson`\r\nand re-importing; see ([the
test\r\nplan](#191116) as well as
a\r\n[reference table
of\r\nscenarios](https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/docs/rfcs/detection_response/prebuilt_rules_customization.md#importing-rules))\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>\r\nCo-authored-by:
Georgii Gorbachev
<[email protected]>","sha":"c0b1301a21b74e8b643fc0e898b1d982fd85d98e"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/190198","number":190198,"mergeCommit":{"message":"[Security
Solution] Allow importing of prebuilt rules via the API (#190198)\n\n##
Summary\r\n\r\nThis PR introduces the backend functionality necessary to
import\r\nprebuilt rules via our existing import API.
The\r\n[RFC](https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/docs/rfcs/detection_response/prebuilt_rules_customization.md)\r\ngoes
into detail, and the import-specific issue is
described\r\n[here](#180168),
but at a high\r\nlevel we're adding two things in this PR:\r\n\r\n1. The
logic to calculate the new `rule_source` field on import,
which\r\ncontains the information about the rule's relationship to
existing\r\nprebuilt rules.\r\n2. A new code path for importing rules,
which respects the calculated\r\n`rule_source` field.\r\n\r\nIn order to
maintain backwards compatibility with the existing import\r\nlogic, and
because the existing import implementation is hard to\r\nmodify/extend,
I opted to add a top-level switch on the feature flag in\r\nthe import
route itself, which calls either the existing import function\r\n(now
named `importRulesLegacy`), or the new function, `importRules`,\r\nwhich
ultimately calls the new `DetectionRulesClient`
method,\r\n`importRules`. Neither knows about the feature flag, and
thanks to great\r\nsuggestions from @xcrzx there are nice, clean
boundaries between the\r\nimport functions and the client
methods.\r\n\r\nI went down the path of trying to write the new import
code by reusing\r\nthe outer `importRules` function, but after
discussion with the team we\r\ndecided it was simplest to simply
bifurcate the code at that point, so\r\nthat we have:\r\n\r\n1. The
legacy import code, which:\r\n * only supports custom rules (`immutable:
false`)\r\n * accepts `version` as an optional parameter\r\n* calculates
a legacy `rule_source` value based on the `immutable` field\r\n2. The
new import code, which\r\n * Installs the prebuilt rules assets as
necessary\r\n * Accepts both kinds of rules (`immutable:
true/false`)\r\n * Requires `version` as a parameter for _prebuilt_
rules\r\n * Allows `version` to be optional for custom rules\r\n *
calculates a proper `rule_source` (and `immutable`)\r\n\r\n\r\n###
Deprecation of `immutable`\r\nThe RFC (and thus I) had initially thought
that we'd be deprecating the\r\n`immutable` field as part of this
PR/Epic. However,
after\r\n[discussion](https://github.com/elastic/kibana/pull/190198#discussion_r1736021749)\r\nwe
have opted to continue supporting `immutable` until such time as
we\r\ncan drop it, compatibility-wise.\r\n\r\n\r\n## Steps to
Review\r\n1. Enable the Feature Flag:
`prebuiltRulesCustomizationEnabled`\r\n2. Install the prebuilt rules
package via fleet\r\n3. Create and export a custom rule to serve as a
template (or download\r\none:\r\n\r\ncurl -L -H 'Authorization:
8eef0fe5d7dfc52b' -o
'rules_export\r\n(1).ndjson'\r\nhttps://upload.elastic.co/d/4693e7fe4356ce7bcf7b7d6b72881a9fd189730c61bf5ef47c9930458d746979\r\n
)\r\n \r\n4. Install some prebuilt rules, and obtain a prebuilt rule's
`rule_id`,\r\ne.g. `ac8805f6-1e08-406c-962e-3937057fa86f`\r\n5. Edit the
`rules_export.ndjson` to contain only the first line, and\r\nmodify the
`rule_id` with the prebuilt rule's\r\n6. Import `rules_export.ndjson`
and then retrieve the rule via the
Dev\r\nConsole:\r\n\r\nGET\r\nkbn:api/detection_engine/rules?rule_id=ac8805f6-1e08-406c-962e-3937057fa86f\r\n\r\n7.
Observe that the rule has the expected `rule_source` and
`immutable`\r\nvalues\r\n8. Test other permutations of import by
modifying `rules_export.ndjson`\r\nand re-importing; see ([the
test\r\nplan](#191116) as well as
a\r\n[reference table
of\r\nscenarios](https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/docs/rfcs/detection_response/prebuilt_rules_customization.md#importing-rules))\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>\r\nCo-authored-by:
Georgii Gorbachev
<[email protected]>","sha":"c0b1301a21b74e8b643fc0e898b1d982fd85d98e"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Ryland Herrick <[email protected]>
  • Loading branch information
kibanamachine and rylnd authored Oct 16, 2024
1 parent bd11946 commit 40e21a7
Show file tree
Hide file tree
Showing 50 changed files with 2,730 additions and 650 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import type { RuleToImport } from './rule_to_import';
import type { RuleToImport, ValidatedRuleToImport } from './rule_to_import';

export const getImportRulesSchemaMock = (rewrites?: Partial<RuleToImport>): RuleToImport =>
({
Expand All @@ -15,12 +15,18 @@ export const getImportRulesSchemaMock = (rewrites?: Partial<RuleToImport>): Rule
severity: 'high',
type: 'query',
risk_score: 55,
language: 'kuery',
rule_id: 'rule-1',
immutable: false,
...rewrites,
} as RuleToImport);

export const getValidatedRuleToImportMock = (
overrides?: Partial<ValidatedRuleToImport>
): ValidatedRuleToImport => ({
version: 1,
...getImportRulesSchemaMock(overrides),
});

export const getImportRulesWithIdSchemaMock = (ruleId = 'rule-1'): RuleToImport => ({
id: '6afb8ce1-ea94-4790-8653-fd0b021d2113',
description: 'some description',
Expand All @@ -29,7 +35,6 @@ export const getImportRulesWithIdSchemaMock = (ruleId = 'rule-1'): RuleToImport
severity: 'high',
type: 'query',
risk_score: 55,
language: 'kuery',
rule_id: ruleId,
immutable: false,
});
Expand Down Expand Up @@ -63,7 +68,6 @@ export const getImportThreatMatchRulesSchemaMock = (
severity: 'high',
type: 'threat_match',
risk_score: 55,
language: 'kuery',
rule_id: 'rule-1',
threat_index: ['index-123'],
threat_mapping: [{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ describe('RuleToImport', () => {
);
});

test('You cannot set the immutable to a number when trying to create a rule', () => {
test('You cannot set immutable to a number', () => {
const payload = getImportRulesSchemaMock({
// @ts-expect-error assign unsupported value
immutable: 5,
Expand All @@ -560,11 +560,11 @@ describe('RuleToImport', () => {
expectParseError(result);

expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"immutable: Invalid literal value, expected false"`
`"immutable: Expected boolean, received number"`
);
});

test('You can optionally set the immutable to be false', () => {
test('You can optionally set immutable to false', () => {
const payload: RuleToImportInput = getImportRulesSchemaMock({
immutable: false,
});
Expand All @@ -574,32 +574,14 @@ describe('RuleToImport', () => {
expectParseSuccess(result);
});

test('You cannot set the immutable to be true', () => {
test('You can optionally set immutable to true', () => {
const payload = getImportRulesSchemaMock({
// @ts-expect-error assign unsupported value
immutable: true,
});

const result = RuleToImport.safeParse(payload);
expectParseError(result);

expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"immutable: Invalid literal value, expected false"`
);
});

test('You cannot set the immutable to be a number', () => {
const payload = getImportRulesSchemaMock({
// @ts-expect-error assign unsupported value
immutable: 5,
});

const result = RuleToImport.safeParse(payload);
expectParseError(result);

expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"immutable: Invalid literal value, expected false"`
);
expectParseSuccess(result);
});

test('You cannot set the risk_score to 101', () => {
Expand Down Expand Up @@ -1091,5 +1073,16 @@ describe('RuleToImport', () => {
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});

describe('backwards compatibility', () => {
it('allows version to be absent', () => {
const payload = getImportRulesSchemaMock();
delete payload.version;

const result = RuleToImport.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import {
RequiredFieldInput,
RuleSignatureId,
TypeSpecificCreateProps,
RuleVersion,
} from '../../model/rule_schema';

/**
* Differences from this and the createRulesSchema are
* - rule_id is required
* - id is optional (but ignored in the import code - rule_id is exclusively used for imports)
* - immutable is optional but if it is any value other than false it will be rejected
* - immutable is optional (but ignored in the import code)
* - created_at is optional (but ignored in the import code)
* - updated_at is optional (but ignored in the import code)
* - created_by is optional (but ignored in the import code)
Expand All @@ -29,7 +30,6 @@ export type RuleToImportInput = z.input<typeof RuleToImport>;
export const RuleToImport = BaseCreateProps.and(TypeSpecificCreateProps).and(
ResponseFields.partial().extend({
rule_id: RuleSignatureId,
immutable: z.literal(false).default(false),
/*
Overriding `required_fields` from ResponseFields because
in ResponseFields `required_fields` has the output type,
Expand All @@ -40,3 +40,19 @@ export const RuleToImport = BaseCreateProps.and(TypeSpecificCreateProps).and(
required_fields: z.array(RequiredFieldInput).optional(),
})
);

/**
* This type represents new rules being imported once the prebuilt rule
* customization work is complete. In order to provide backwards compatibility
* with existing rules, and not change behavior, we now validate `version` in
* the route as opposed to the type itself.
*
* It differs from RuleToImport in that it requires a `version` field.
*/
export type ValidatedRuleToImport = z.infer<typeof ValidatedRuleToImport>;
export type ValidatedRuleToImportInput = z.input<typeof ValidatedRuleToImport>;
export const ValidatedRuleToImport = RuleToImport.and(
z.object({
version: RuleVersion,
})
);
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import type { RuleToImport } from './rule_to_import';
import type { RuleToImport, ValidatedRuleToImport } from './rule_to_import';

/**
* Additional validation that is implemented outside of the schema itself.
Expand Down Expand Up @@ -55,3 +55,6 @@ const validateThreshold = (rule: RuleToImport): string[] => {
}
return errors;
};

export const ruleToImportHasVersion = (rule: RuleToImport): rule is ValidatedRuleToImport =>
!!rule.version;
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const MAX_PREBUILT_RULES_COUNT = 10_000;
export interface IPrebuiltRuleAssetsClient {
fetchLatestAssets: () => Promise<PrebuiltRuleAsset[]>;

fetchLatestVersions(): Promise<RuleVersionSpecifier[]>;
fetchLatestVersions(ruleIds?: string[]): Promise<RuleVersionSpecifier[]>;

fetchAssetsByVersion(versions: RuleVersionSpecifier[]): Promise<PrebuiltRuleAsset[]>;
}
Expand Down Expand Up @@ -72,8 +72,12 @@ export const createPrebuiltRuleAssetsClient = (
});
},

fetchLatestVersions: (): Promise<RuleVersionSpecifier[]> => {
fetchLatestVersions: (ruleIds: string[] = []): Promise<RuleVersionSpecifier[]> => {
return withSecuritySpan('IPrebuiltRuleAssetsClient.fetchLatestVersions', async () => {
const filter = ruleIds
.map((ruleId) => `${PREBUILT_RULE_ASSETS_SO_TYPE}.attributes.rule_id: ${ruleId}`)
.join(' OR ');

const findResult = await savedObjectsClient.find<
PrebuiltRuleAsset,
{
Expand All @@ -83,6 +87,7 @@ export const createPrebuiltRuleAssetsClient = (
}
>({
type: PREBUILT_RULE_ASSETS_SO_TYPE,
filter,
aggs: {
rules: {
terms: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { getRulesSchemaMock } from '../../../../../../../common/api/detection_engine/model/rule_schema/rule_response_schema.mock';

import type { requestMock } from '../../../../routes/__mocks__';
import { createMockConfig, requestContextMock, serverMock } from '../../../../routes/__mocks__';
import { configMock, requestContextMock, serverMock } from '../../../../routes/__mocks__';
import { buildHapiStream } from '../../../../routes/__mocks__/utils';
import {
getImportRulesRequest,
Expand All @@ -26,23 +26,30 @@ import {
getBasicEmptySearchResponse,
} from '../../../../routes/__mocks__/request_responses';

import * as createRulesAndExceptionsStreamFromNdJson from '../../../logic/import/create_rules_stream_from_ndjson';
import * as createPromiseFromRuleImportStream from '../../../logic/import/create_promise_from_rule_import_stream';
import { getQueryRuleParams } from '../../../../rule_schema/mocks';
import { importRulesRoute } from './route';
import { HttpAuthzError } from '../../../../../machine_learning/validation';
import { createPrebuiltRuleAssetsClient as createPrebuiltRuleAssetsClientMock } from '../../../../prebuilt_rules/logic/rule_assets/__mocks__/prebuilt_rule_assets_client';

jest.mock('../../../../../machine_learning/authz');

let mockPrebuiltRuleAssetsClient: ReturnType<typeof createPrebuiltRuleAssetsClientMock>;

jest.mock('../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client', () => ({
createPrebuiltRuleAssetsClient: () => mockPrebuiltRuleAssetsClient,
}));

describe('Import rules route', () => {
let config: ReturnType<typeof createMockConfig>;
let config: ReturnType<typeof configMock.createDefault>;
let server: ReturnType<typeof serverMock.create>;
let request: ReturnType<typeof requestMock.create>;
let { clients, context } = requestContextMock.createTools();

beforeEach(() => {
server = serverMock.create();
({ clients, context } = requestContextMock.createTools());
config = createMockConfig();
config = configMock.createDefault();
const hapiStream = buildHapiStream(ruleIdsToNdJsonString(['rule-1']));
request = getImportRulesRequest(hapiStream);

Expand All @@ -54,6 +61,7 @@ describe('Import rules route', () => {
context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue(
elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse())
);
mockPrebuiltRuleAssetsClient = createPrebuiltRuleAssetsClientMock();
importRulesRoute(server.router, config);
});

Expand Down Expand Up @@ -112,9 +120,9 @@ describe('Import rules route', () => {
});
});

test('returns error if createRulesAndExceptionsStreamFromNdJson throws error', async () => {
test('returns error if createPromiseFromRuleImportStream throws error', async () => {
const transformMock = jest
.spyOn(createRulesAndExceptionsStreamFromNdJson, 'createRulesAndExceptionsStreamFromNdJson')
.spyOn(createPromiseFromRuleImportStream, 'createPromiseFromRuleImportStream')
.mockImplementation(() => {
throw new Error('Test error');
});
Expand All @@ -133,6 +141,30 @@ describe('Import rules route', () => {
expect(response.status).toEqual(400);
expect(response.body).toEqual({ message: 'Invalid file extension .html', status_code: 400 });
});

describe('with prebuilt rules customization enabled', () => {
beforeEach(() => {
clients.detectionRulesClient.importRules.mockResolvedValueOnce([]);
server = serverMock.create(); // old server already registered this route
config = configMock.withExperimentalFeature(config, 'prebuiltRulesCustomizationEnabled');

importRulesRoute(server.router, config);
});

test('returns 500 if importing fails', async () => {
clients.detectionRulesClient.importRules
.mockReset()
.mockRejectedValue(new Error('test error'));

const response = await server.inject(request, requestContextMock.convertContext(context));

expect(response.status).toEqual(500);
expect(response.body).toMatchObject({
message: 'test error',
status_code: 500,
});
});
});
});

describe('single rule import', () => {
Expand Down
Loading

0 comments on commit 40e21a7

Please sign in to comment.