[ResponseOps][Rules] Ignore unknowns in the schema of the log threshold params (#217440)

## Summary

A PR introduced into 8.18/9.0
(https://github.com/elastic/kibana/pull/205507) changed the way we
validate the log threshold rule type parameters. The validation happens
on rule params and changes a loose validation to a strict validation, so
those users who’ve inserted excess fields before 8.18/9.0 will see rules
starting to fail to run, their rule page failing to load and the API
starting to reject calls with excess fields.

Fixes: https://github.com/elastic/kibana/issues/217384

## Testing instructions

1. Start Kibana on 8.17 and create the following rule using the API. Let
the rule run.

<details><summary>Rule</summary>

```
{
    "name": "[QAF] Observability rule 3",
    "tags": [
        "metrics",
        "threshold",
        "qaf"
    ],
    "rule_type_id": "logs.alert.document.count",
    "consumer": "alerts",
    "schedule": {
        "interval": "1m"
    },
    "actions": [],
    "params": {
        "timeSize": 8,
        "timeUnit": "h",
        "count": {
            "value": 1,
            "comparator": "more than"
        },
        "criteria": [
            {
                "field": "bytes",
                "comparator": "more than",
                "value": 1
            }
        ],
        "logView": {
            "logViewId": "log-view-reference-0",
            "type": "log-view-reference"
        },
        "groupBy": [
            "geo.dest"
        ],
        "outputIndex": ".alerts-observability.logs.alerts-default"
    }
}
```

</details> 

2. Start Kibana on 8.18. Verify that you cannot create the same rule and
the rule created in step 1 starts failing.
3. Start Kibana on this PR and that you can create the same rule and the
rule created in step 1 is working as expected.

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] [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
This commit is contained in:
Christos Nasikas 2025-04-08 16:58:26 +03:00 committed by GitHub
parent fd374463f7
commit 2a01722cfa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 191 additions and 38 deletions

View file

@ -0,0 +1,145 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { omit } from 'lodash';
import { logThresholdParamsSchema } from './v1';
describe('logThresholdParamsSchema', () => {
const base = {
timeSize: 8,
timeUnit: 'h',
count: {
value: 1,
comparator: 'more than',
},
logView: {
logViewId: 'log-view-reference-0',
type: 'log-view-reference',
},
groupBy: ['geo.dest'],
};
const countParams = {
...base,
criteria: [
{
field: 'bytes',
comparator: 'more than',
value: 1,
},
],
};
const ratioParams = {
...base,
criteria: [
[
{
field: 'bytes',
comparator: 'more than',
value: 1,
},
],
],
};
const countParamsWithExcess = {
...countParams,
outputIndex: '.alerts-observability.logs.alerts-default',
};
const ratioParamsWithExcess = {
...ratioParams,
outputIndex: '.alerts-observability.logs.alerts-default',
};
it('validates count params correctly', () => {
expect(() => logThresholdParamsSchema.validate(countParams)).not.toThrow();
});
it('validates ratio params correctly', () => {
expect(() => logThresholdParamsSchema.validate(ratioParams)).not.toThrow();
});
it('does not throw with excess fields in count params', () => {
const result = logThresholdParamsSchema.validate(countParamsWithExcess);
expect(result).toEqual(omit(countParamsWithExcess, 'outputIndex'));
});
it('does not throw with excess fields in ration params', () => {
const result = logThresholdParamsSchema.validate(ratioParamsWithExcess);
expect(result).toEqual(omit(ratioParamsWithExcess, 'outputIndex'));
});
it('strips out excess fields in count params', () => {
const result = logThresholdParamsSchema.validate(countParamsWithExcess);
expect(result).toEqual(omit(countParamsWithExcess, 'outputIndex'));
});
it('strips out excess fields in ratio params', () => {
const result = logThresholdParamsSchema.validate(ratioParamsWithExcess);
expect(result).toEqual(omit(ratioParamsWithExcess, 'outputIndex'));
});
it.each(['criteria', 'count', 'timeUnit', 'timeSize', 'logView'])(
'fails without %s required field in count params',
(field) => {
expect(() => logThresholdParamsSchema.validate(omit(countParams, field))).toThrow();
}
);
it.each(['groupBy'])('does not fail without %s optional field in count params', (field) => {
expect(() => logThresholdParamsSchema.validate(omit(countParams, field))).not.toThrow();
});
it.each(['criteria', 'count', 'timeUnit', 'timeSize', 'logView'])(
'fails without %s required field in ratio params',
(field) => {
expect(() => logThresholdParamsSchema.validate(omit(ratioParams, field))).toThrow();
}
);
it.each(['groupBy'])('does not fail without %s optional field in ratio params', (field) => {
expect(() => logThresholdParamsSchema.validate(omit(ratioParams, field))).not.toThrow();
});
it('trips out excess fields in logView', () => {
expect(() =>
logThresholdParamsSchema.validate({
...countParams,
logView: { ...countParams.logView, excessField: 'foo' },
})
).not.toThrow();
});
it('trips out excess fields in threshold', () => {
expect(() =>
logThresholdParamsSchema.validate({
...countParams,
count: { ...countParams.count, excessField: 'foo' },
})
).not.toThrow();
});
it('trips out excess fields in criteria', () => {
expect(() =>
logThresholdParamsSchema.validate({
...countParams,
criteria: [
{
field: 'bytes',
comparator: 'more than',
value: 1,
excessField: 'foo',
},
],
})
).not.toThrow();
});
});

View file

@ -9,10 +9,13 @@
import { schema } from '@kbn/config-schema';
const persistedLogViewReferenceSchema = schema.object({
logViewId: schema.string(),
type: schema.literal('log-view-reference'),
});
const persistedLogViewReferenceSchema = schema.object(
{
logViewId: schema.string(),
type: schema.literal('log-view-reference'),
},
{ unknowns: 'ignore' }
);
// Comparators //
enum Comparator {
@ -41,16 +44,22 @@ const ComparatorSchema = schema.oneOf([
schema.literal(Comparator.NOT_MATCH_PHRASE),
]);
const ThresholdSchema = schema.object({
comparator: ComparatorSchema,
value: schema.number(),
});
const ThresholdSchema = schema.object(
{
comparator: ComparatorSchema,
value: schema.number(),
},
{ unknowns: 'ignore' }
);
const criterionSchema = schema.object({
field: schema.string(),
comparator: ComparatorSchema,
value: schema.oneOf([schema.string(), schema.number()]),
});
const criterionSchema = schema.object(
{
field: schema.string(),
comparator: ComparatorSchema,
value: schema.oneOf([schema.string(), schema.number()]),
},
{ unknowns: 'ignore' }
);
const countCriteriaSchema = schema.arrayOf(criterionSchema);
const ratioCriteriaSchema = schema.arrayOf(countCriteriaSchema);
@ -65,34 +74,33 @@ const timeUnitSchema = schema.oneOf([
const timeSizeSchema = schema.number();
const groupBySchema = schema.arrayOf(schema.string());
const RequiredRuleParamsSchema = schema.object({
// NOTE: "count" would be better named as "threshold", but this would require a
// migration of encrypted saved objects, so we'll keep "count" until it's problematic.
count: ThresholdSchema,
timeUnit: timeUnitSchema,
timeSize: timeSizeSchema,
logView: persistedLogViewReferenceSchema, // Alerts are only compatible with persisted Log Views
});
const OptionalRuleParamsSchema = schema.object({
groupBy: schema.maybe(groupBySchema),
});
const countRuleParamsSchema = schema.intersection([
schema.object({
const countRuleParamsSchema = schema.object(
{
criteria: countCriteriaSchema,
}),
RequiredRuleParamsSchema,
OptionalRuleParamsSchema,
]);
// NOTE: "count" would be better named as "threshold", but this would require a
// migration of encrypted saved objects, so we'll keep "count" until it's problematic.
count: ThresholdSchema,
timeUnit: timeUnitSchema,
timeSize: timeSizeSchema,
logView: persistedLogViewReferenceSchema, // Alerts are only compatible with persisted Log Views
groupBy: schema.maybe(groupBySchema),
},
{ unknowns: 'ignore' }
);
const ratioRuleParamsSchema = schema.intersection([
schema.object({
const ratioRuleParamsSchema = schema.object(
{
criteria: ratioCriteriaSchema,
}),
RequiredRuleParamsSchema,
OptionalRuleParamsSchema,
]);
// NOTE: "count" would be better named as "threshold", but this would require a
// migration of encrypted saved objects, so we'll keep "count" until it's problematic.
count: ThresholdSchema,
timeUnit: timeUnitSchema,
timeSize: timeSizeSchema,
logView: persistedLogViewReferenceSchema, // Alerts are only compatible with persisted Log Views
groupBy: schema.maybe(groupBySchema),
},
{ unknowns: 'ignore' }
);
export const logThresholdParamsSchema = schema.oneOf([
countRuleParamsSchema,