[8.18] [Security Solution] Normalizes `filters` field before rule diff comparison (#206344) (#209531)

# Backport

This will backport the following commits from `main` to `8.18`:
- [[Security Solution] Normalizes `filters` field before rule
diff comparison
(#206344)](https://github.com/elastic/kibana/pull/206344)

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

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

<!--BACKPORT [{"author":{"name":"Davis
Plumlee","email":"56367316+dplumlee@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-02-04T12:40:52Z","message":"[Security
Solution] Normalizes `filters` field before rule diff comparison
(#206344)\n\n**Fixes:
https://github.com/elastic/kibana/issues/202966**\r\n**Fixes:
https://github.com/elastic/kibana/issues/206527**\r\n\r\n##
Summary\r\n\r\nThe issue that causes the overarching problem mentioned
in the ticket is\r\nthat we add an extra `alias: null` property to the
filter via the kibana\r\nfilter utils instead of keeping the `alias`
field unset. This is\r\nfunctionally the same rule but since the
prebuilt rule objects are\r\ntechnically different (`alias` is set to
`undefined` instead of `null`),\r\nwe mark these rules as customized and
causes the query fields to show as\r\na modified field on
update.\r\n\r\nTo address this, since changing the kibana util filter
would be very\r\ninvasive and touching a lot of code, we instead
normalize the field on\r\nour side before version comparison. This fixes
the bug reported and\r\nimproves resiliency of rule upgrades in the
future.\r\n\r\n### Testing (copied from ticket) \r\n\r\n- Ensure the
`prebuiltRulesCustomizationEnabled` feature flag is enabled\r\n- Allow
internal APIs via adding `server.restrictInternalApis: false`
to\r\n`kibana.dev.yaml`\r\n- Clear Elasticsearch data\r\n- Run
Elasticsearch and Kibana locally (do not open Kibana in a
web\r\nbrowser)\r\n- Install an outdated version of the
`security_detection_engine` Fleet\r\npackage\r\n```bash\r\ncurl -X POST
--user elastic:changeme -H 'Content-Type: application/json' -H
'kbn-xsrf: 123' -H \"elastic-api-version: 2023-10-31\" -d
'{\"force\":true}'
http://localhost:5601/kbn/api/fleet/epm/packages/security_detection_engine/8.14.1\r\n```\r\n\r\n-
Install prebuilt rules\r\n```bash\r\ncurl -X POST --user
elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'
-H \"elastic-api-version: 1\" -d '{\"mode\":\"ALL_RULES\"}'
http://localhost:5601/kbn/internal/detection_engine/prebuilt_rules/installation/_perform\r\n```\r\n\r\n-
Open a `threat_match` rule for editing. For example `Threat Intel
Hash\r\nIndicator Match` with rule_id
`aab184d3-72b3-4639-b242-6597c99d8bca`.\r\n\r\nWith this fix, users
should **NOT** see any extra fields in the rule\r\nupgrade flyout, nor
should the rule be marked as \"Modified\" if opened\r\nand saved with no
other modifications\r\n\r\n### Checklist\r\n\r\nCheck the PR satisfies
following conditions. \r\n\r\nReviewers should verify this PR satisfies
this list as well.\r\n\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\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"a5b4570cd86b8c23d1687f4b2a6b1fec47876dcc","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","v9.0.0","Team:Detections
and Resp","Team: SecuritySolution","Team:Detection Rule
Management","Feature:Prebuilt Detection
Rules","backport:version","v8.18.0","v9.1.0","v8.19.0"],"title":"[Security
Solution] Normalizes `filters` field before rule diff comparison
","number":206344,"url":"https://github.com/elastic/kibana/pull/206344","mergeCommit":{"message":"[Security
Solution] Normalizes `filters` field before rule diff comparison
(#206344)\n\n**Fixes:
https://github.com/elastic/kibana/issues/202966**\r\n**Fixes:
https://github.com/elastic/kibana/issues/206527**\r\n\r\n##
Summary\r\n\r\nThe issue that causes the overarching problem mentioned
in the ticket is\r\nthat we add an extra `alias: null` property to the
filter via the kibana\r\nfilter utils instead of keeping the `alias`
field unset. This is\r\nfunctionally the same rule but since the
prebuilt rule objects are\r\ntechnically different (`alias` is set to
`undefined` instead of `null`),\r\nwe mark these rules as customized and
causes the query fields to show as\r\na modified field on
update.\r\n\r\nTo address this, since changing the kibana util filter
would be very\r\ninvasive and touching a lot of code, we instead
normalize the field on\r\nour side before version comparison. This fixes
the bug reported and\r\nimproves resiliency of rule upgrades in the
future.\r\n\r\n### Testing (copied from ticket) \r\n\r\n- Ensure the
`prebuiltRulesCustomizationEnabled` feature flag is enabled\r\n- Allow
internal APIs via adding `server.restrictInternalApis: false`
to\r\n`kibana.dev.yaml`\r\n- Clear Elasticsearch data\r\n- Run
Elasticsearch and Kibana locally (do not open Kibana in a
web\r\nbrowser)\r\n- Install an outdated version of the
`security_detection_engine` Fleet\r\npackage\r\n```bash\r\ncurl -X POST
--user elastic:changeme -H 'Content-Type: application/json' -H
'kbn-xsrf: 123' -H \"elastic-api-version: 2023-10-31\" -d
'{\"force\":true}'
http://localhost:5601/kbn/api/fleet/epm/packages/security_detection_engine/8.14.1\r\n```\r\n\r\n-
Install prebuilt rules\r\n```bash\r\ncurl -X POST --user
elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'
-H \"elastic-api-version: 1\" -d '{\"mode\":\"ALL_RULES\"}'
http://localhost:5601/kbn/internal/detection_engine/prebuilt_rules/installation/_perform\r\n```\r\n\r\n-
Open a `threat_match` rule for editing. For example `Threat Intel
Hash\r\nIndicator Match` with rule_id
`aab184d3-72b3-4639-b242-6597c99d8bca`.\r\n\r\nWith this fix, users
should **NOT** see any extra fields in the rule\r\nupgrade flyout, nor
should the rule be marked as \"Modified\" if opened\r\nand saved with no
other modifications\r\n\r\n### Checklist\r\n\r\nCheck the PR satisfies
following conditions. \r\n\r\nReviewers should verify this PR satisfies
this list as well.\r\n\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\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"a5b4570cd86b8c23d1687f4b2a6b1fec47876dcc"}},"sourceBranch":"main","suggestedTargetBranches":["9.0","8.18","8.x"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/206344","number":206344,"mergeCommit":{"message":"[Security
Solution] Normalizes `filters` field before rule diff comparison
(#206344)\n\n**Fixes:
https://github.com/elastic/kibana/issues/202966**\r\n**Fixes:
https://github.com/elastic/kibana/issues/206527**\r\n\r\n##
Summary\r\n\r\nThe issue that causes the overarching problem mentioned
in the ticket is\r\nthat we add an extra `alias: null` property to the
filter via the kibana\r\nfilter utils instead of keeping the `alias`
field unset. This is\r\nfunctionally the same rule but since the
prebuilt rule objects are\r\ntechnically different (`alias` is set to
`undefined` instead of `null`),\r\nwe mark these rules as customized and
causes the query fields to show as\r\na modified field on
update.\r\n\r\nTo address this, since changing the kibana util filter
would be very\r\ninvasive and touching a lot of code, we instead
normalize the field on\r\nour side before version comparison. This fixes
the bug reported and\r\nimproves resiliency of rule upgrades in the
future.\r\n\r\n### Testing (copied from ticket) \r\n\r\n- Ensure the
`prebuiltRulesCustomizationEnabled` feature flag is enabled\r\n- Allow
internal APIs via adding `server.restrictInternalApis: false`
to\r\n`kibana.dev.yaml`\r\n- Clear Elasticsearch data\r\n- Run
Elasticsearch and Kibana locally (do not open Kibana in a
web\r\nbrowser)\r\n- Install an outdated version of the
`security_detection_engine` Fleet\r\npackage\r\n```bash\r\ncurl -X POST
--user elastic:changeme -H 'Content-Type: application/json' -H
'kbn-xsrf: 123' -H \"elastic-api-version: 2023-10-31\" -d
'{\"force\":true}'
http://localhost:5601/kbn/api/fleet/epm/packages/security_detection_engine/8.14.1\r\n```\r\n\r\n-
Install prebuilt rules\r\n```bash\r\ncurl -X POST --user
elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123'
-H \"elastic-api-version: 1\" -d '{\"mode\":\"ALL_RULES\"}'
http://localhost:5601/kbn/internal/detection_engine/prebuilt_rules/installation/_perform\r\n```\r\n\r\n-
Open a `threat_match` rule for editing. For example `Threat Intel
Hash\r\nIndicator Match` with rule_id
`aab184d3-72b3-4639-b242-6597c99d8bca`.\r\n\r\nWith this fix, users
should **NOT** see any extra fields in the rule\r\nupgrade flyout, nor
should the rule be marked as \"Modified\" if opened\r\nand saved with no
other modifications\r\n\r\n### Checklist\r\n\r\nCheck the PR satisfies
following conditions. \r\n\r\nReviewers should verify this PR satisfies
this list as well.\r\n\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\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"a5b4570cd86b8c23d1687f4b2a6b1fec47876dcc"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Davis Plumlee <56367316+dplumlee@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2025-02-05 01:40:33 +11:00 committed by GitHub
parent f6e739b19e
commit f25de199b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 182 additions and 6 deletions

View file

@ -75,7 +75,7 @@ export function mapFilter(filter: Filter) {
mappedFilter.meta.params = mapped.params;
mappedFilter.meta.disabled = Boolean(mappedFilter.meta.disabled);
mappedFilter.meta.negate = Boolean(mappedFilter.meta.negate);
mappedFilter.meta.alias = mappedFilter.meta.alias || null;
mappedFilter.meta.alias = mappedFilter.meta.alias;
return mappedFilter;
}

View file

@ -44,7 +44,7 @@ describe('filter manager utilities', () => {
"params": Array [
Object {
"meta": Object {
"alias": null,
"alias": undefined,
"disabled": false,
"index": "logstash-*",
"key": "bytes",

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import type { Filter } from '@kbn/es-query';
import { KqlQueryType } from '../../../api/detection_engine';
import {
extractRuleEqlQuery,
@ -12,6 +13,24 @@ import {
extractRuleKqlQuery,
} from './extract_rule_data_query';
const mockFilter: Filter = {
meta: {
alias: null,
negate: false,
disabled: false,
type: 'phrase',
key: 'test',
params: {
query: 'value',
},
},
query: {
term: {
field: 'value',
},
},
};
describe('extract rule data queries', () => {
describe('extractRuleKqlQuery', () => {
it('extracts a trimmed version of the query field for inline query types', () => {
@ -24,6 +43,39 @@ describe('extract rule data queries', () => {
filters: [],
});
});
it('normalizes filters', () => {
const extractedKqlQuery = extractRuleKqlQuery(
'event.kind:alert',
'kuery',
[mockFilter],
undefined
);
expect(extractedKqlQuery).toEqual({
type: KqlQueryType.inline_query,
query: 'event.kind:alert',
language: 'kuery',
filters: [
{
meta: {
negate: false,
disabled: false,
type: 'phrase',
key: 'test',
params: {
query: 'value',
},
},
query: {
term: {
field: 'value',
},
},
},
],
});
});
});
describe('extractRuleEqlQuery', () => {

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import type { Filter } from '@kbn/es-query';
import type {
EqlQueryLanguage,
EsqlQueryLanguage,
@ -48,7 +49,7 @@ export const extractInlineKqlQuery = (
type: KqlQueryType.inline_query,
query: query?.trim() ?? '',
language: language ?? 'kuery',
filters: filters ?? [],
filters: normalizeFilterArray(filters),
};
};
@ -65,7 +66,7 @@ export const extractRuleEqlQuery = (params: ExtractRuleEqlQueryParams): RuleEqlQ
return {
query: params.query.trim(),
language: params.language,
filters: params.filters ?? [],
filters: normalizeFilterArray(params.filters),
event_category_override: params.eventCategoryOverride,
timestamp_field: params.timestampField,
tiebreaker_field: params.tiebreakerField,
@ -81,3 +82,22 @@ export const extractRuleEsqlQuery = (
language,
};
};
/**
* Removes the null `alias` field that gets appended from the internal kibana filter util for comparison
* Relevant issue: https://github.com/elastic/kibana/issues/202966
*/
const normalizeFilterArray = (filters: RuleFilterArray | undefined): RuleFilterArray => {
if (!filters?.length) {
return [];
}
return (filters as Filter[]).map((filter) => ({
...filter,
meta: filter.meta
? {
...filter.meta,
alias: filter.meta.alias ?? undefined,
}
: undefined,
}));
};

View file

@ -175,8 +175,8 @@ export const QueryBarField = ({
// if saved query fetched, reset values in queryBar input and filters to saved query's values
useEffect(() => {
if (resetToSavedQuery && savedQuery) {
const newFiledValue = savedQueryToFieldValue(savedQuery);
setFieldValue(newFiledValue);
const newFieldValue = savedQueryToFieldValue(savedQuery);
setFieldValue(newFieldValue);
}
}, [resetToSavedQuery, savedQuery, setFieldValue]);

View file

@ -179,6 +179,110 @@ export default ({ getService }: FtrProviderContext): void => {
expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0);
});
});
describe('when all query versions have filters with alias fields set to null', () => {
it('should not show in the upgrade/_review API response', async () => {
// Install base prebuilt detection rule
await createHistoricalPrebuiltRuleAssetSavedObjects(es, [
createRuleAssetSavedObject({
rule_id: 'rule-1',
version: 1,
type: 'query',
query: 'query string = true',
language: 'kuery',
filters: [
{
meta: {
negate: false,
disabled: false,
type: 'phrase',
key: 'test',
params: {
query: 'value',
},
},
query: {
term: {
field: 'value',
},
},
},
],
}),
]);
await installPrebuiltRules(es, supertest);
// Customize a kql_query field on the installed rule
await updateRule(supertest, {
...getPrebuiltRuleMock(),
rule_id: 'rule-1',
type: 'query',
query: 'query string = true',
language: 'kuery',
filters: [
{
meta: {
alias: null,
negate: false,
disabled: false,
type: 'phrase',
key: 'test',
params: {
query: 'value',
},
},
query: {
term: {
field: 'value',
},
},
},
],
saved_id: undefined,
} as RuleUpdateProps);
// Add a v2 rule asset to make the upgrade possible, do NOT update the related kql_query field, and create the new rule assets
const updatedRuleAssetSavedObjects = [
createRuleAssetSavedObject({
rule_id: 'rule-1',
version: 2,
type: 'query',
query: 'query string = true',
language: 'kuery',
filters: [
{
meta: {
negate: false,
disabled: false,
type: 'phrase',
key: 'test',
params: {
query: 'value',
},
},
query: {
term: {
field: 'value',
},
},
},
],
}),
];
await createHistoricalPrebuiltRuleAssetSavedObjects(es, updatedRuleAssetSavedObjects);
// Call the upgrade review prebuilt rules endpoint and check that there is 1 rule eligible for update but kql_query field is NOT returned
const reviewResponse = await reviewPrebuiltRulesToUpgrade(supertest);
const fieldDiffObject = reviewResponse.rules[0].diff.fields as AllFieldsDiff;
expect(fieldDiffObject.kql_query).toBeUndefined();
expect(reviewResponse.rules[0].diff.num_fields_with_updates).toBe(1); // `version` is considered an updated field
expect(reviewResponse.rules[0].diff.num_fields_with_conflicts).toBe(0);
expect(reviewResponse.rules[0].diff.num_fields_with_non_solvable_conflicts).toBe(0);
expect(reviewResponse.stats.num_rules_with_conflicts).toBe(0);
expect(reviewResponse.stats.num_rules_with_non_solvable_conflicts).toBe(0);
});
});
});
describe("when rule field doesn't have an update but has a custom value - scenario ABA", () => {