Skip to content

Commit

Permalink
[Security Solution] Handle negative lookback in rule upgrade flyout (#…
Browse files Browse the repository at this point in the history
…204317)

**Fixes: #202715
**Fixes: #204714

## Summary

This PR makes inconsistent/wrong rule's look-back duration prominent for
a user. It falls back to a default 1 minute value in rule upgrade
workflow.

## Details

### Negative/wrong `lookback` problem

There is a difference between rule schedule value in a saved object and
value represented to users

- Saved object (and rule management API) has `interval`, `from` and `to`
fields representing rule schedule. `interval` shows how often a rule
runs in task runner. `from` and `to` stored in date math format like
`now-10m` represent a date time range used to fetch source events. Task
manager strives to run rules exactly every `interval` but it's not
always possible due to multiple reasons like system load and various
delays. To avoid any gaps to appear `from` point in time usually stands
earlier than current time minus `interval`, for example `interval` is
`10 minutes` and `from` is `now-12m` meaning rule will analyze events
starting from 12 minutes old. `to` represents the latest point in time
source events will be analyzed.
- Diffable rule and UI represent rule schedule as `interval` and
`lookback`. Where `interval` is the same as above and `lookback` and a
time duration before current time minus `interval`. For example
`interval` is `10 minutes` and lookback is `2 minutes` it means a rule
will analyzing events starting with 12 minutes old until the current
moment in time.

Literally `interval`, `from` and `to` mean a rule runs every `interval`
and analyzes events starting from `from` until `to`. Technically `from`
and `to` may not have any correlation with `interval`, for example a
rule may analyze one year old events. While it's reasonable for manual
rule runs and gap remediation the same approach doesn't work well for
usual rule schedule. Transformation between `interval`/`from`/`to` and
`interval`/`lookback` works only when `to` is equal the current moment
in time i.e. `now`.

Rule management APIs allow to set any `from` and `to` values resulting
in inconsistent rule schedule. Transformed `interval`/`lookback` value
won't represent real time interval used to fetch source events for
analysis. On top of that negative `lookback` value may puzzle users on
the meaning of the negative sign.

### Prebuilt rules with `interval`/`from`/`to` resulting in negative
`lookback`

Some prebuilt rules have such `interval`, `from` and `to` field values
thatnegative `lookback` is expected, for example `Multiple Okta Sessions
Detected for a Single User`. It runs every `60 minutes` but has `from`
field set to `now-30m` and `to` equals `now`. In the end we have
`lookback` equals `to` - `from` - `interval` = `30 minutes` - `60
minutes` = `-30 minutes`.

Our UI doesn't handle negative `lookback` values. It simply discards a
negative sign and substitutes the rest for editing. In the case above
`30 minutes` will be suggested for editing. Saving the form will result
in changing `from` to `now-90m`

<img width="1712" alt="image"
src="/~https://github.com/user-attachments/assets/05519743-9562-4874-8a73-5596eeccacf2"
/>

### Changes in this PR

This PR mitigates rule schedule inconsistencies caused by `to` fields
not using the current point in time i.e. `now`. The following was done

- `DiffableRule`'s `rule_schedule` was changed to have `interval`,
`from` and `to` fields instead of `interval` and `lookback`
- `_perform` rule upgrade API endpoint was adapted to the new
`DIffableRule`'s `rule_schedule`
- Rule upgrade flyout calculates and shows `interval` and `lookback` in
Diff View, readonly view and field form when `lookback` is non-negative
and `to` equals `now`
- Rule upgrade flyout shows `interval`, `from` and `to` in Diff View,
readonly view and field form when `to` isn't equal `now` or calculated
`lookback` is negative
- Rule upgrade flyout shows a warning when `to` isn't equal `now` or
calculated `lookback` is negative
- Rule upgrade flyout's JSON Diff shows `interval` and `lookback` when
`lookback` is non-negative and `to` equals `now` and shows `interval`,
`from` and `to` in any other case
- Rule details page shows `interval`, `from` and `to` in Diff View,
readonly view and field form when `to` isn't equal `now` or calculated
`lookback` is negative
- `maxValue` was added to `ScheduleItemField` to have an ability to
restrict input at reasonable values

## Screenshots

- Rule upgrade workflow (negative look-back)

<img width="2558" alt="Screenshot 2025-01-02 at 13 16 59"
src="/~https://github.com/user-attachments/assets/b8bf727f-11ca-424f-892b-b024ba7f847a"
/>

<img width="2553" alt="Screenshot 2025-01-02 at 13 17 20"
src="/~https://github.com/user-attachments/assets/9f751ea4-0ce0-4a23-a3b7-0a16494d957e"
/>

<img width="2558" alt="Screenshot 2025-01-02 at 13 18 24"
src="/~https://github.com/user-attachments/assets/6908ab02-4011-4a6e-85ce-e60d5eac7993"
/>

- Rule upgrade workflow (positive look-back)

<img width="2555" alt="Screenshot 2025-01-02 at 13 19 12"
src="/~https://github.com/user-attachments/assets/06208210-c6cd-4842-8aef-6ade5d13bd36"
/>

<img width="2558" alt="Screenshot 2025-01-02 at 13 25 31"
src="/~https://github.com/user-attachments/assets/aed38bb0-ccfb-479a-bb3b-e5442c518e63"
/>

- JSON view

<img width="2559" alt="Screenshot 2025-01-02 at 13 31 37"
src="/~https://github.com/user-attachments/assets/07575a81-676f-418e-8b98-48eefe11ab00"
/>

- Rule details page

<img width="2555" alt="Screenshot 2025-01-02 at 13 13 16"
src="/~https://github.com/user-attachments/assets/e977b752-9d50-4049-917a-af2e8e3f0dfe"
/>

<img width="2558" alt="Screenshot 2025-01-02 at 13 14 10"
src="/~https://github.com/user-attachments/assets/06d6f477-5730-48ca-a240-b5e7592bf173"
/>

## How to test?

- Ensure the `prebuiltRulesCustomizationEnabled` feature flag is enabled
- Allow internal APIs via adding `server.restrictInternalApis: false` to
`kibana.dev.yaml`
- Clear Elasticsearch data
- Run Elasticsearch and Kibana locally (do not open Kibana in a web
browser)
- Install an outdated version of the `security_detection_engine` Fleet
package
```bash
curl -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
```

- Install prebuilt rules
```bash
curl -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
```

- Set "inconsistent" rule schedule for `Suspicious File Creation via
Kworker` rule by running a query below
```bash
curl -X PATCH --user elastic:changeme -H "Content-Type: application/json" -H "elastic-api-version: 2023-10-31" -H "kbn-xsrf: 123" -d '{"rule_id":"ae343298-97bc-47bc-9ea2-5f2ad831c16e","interval":"10m","from":"now-5m","to":"now-2m"}' http://localhost:5601/kbn/api/detection_engine/rules
```

- Open rule upgrade flyout for `Suspicious File Creation via Kworker`
rule

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
maximpn and elasticmachine authored Jan 20, 2025
1 parent a0bdc19 commit 30bb71a
Show file tree
Hide file tree
Showing 57 changed files with 1,838 additions and 614 deletions.
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -984,7 +984,7 @@ x-pack/solutions/security/packages/kbn-securitysolution-list-hooks @elastic/secu
x-pack/solutions/security/packages/kbn-securitysolution-list-utils @elastic/security-detection-engine
x-pack/solutions/security/packages/kbn-securitysolution-lists-common @elastic/security-detection-engine
x-pack/solutions/security/packages/kbn-securitysolution-t-grid @elastic/security-detection-engine
x-pack/solutions/security/packages/kbn-securitysolution-utils @elastic/security-detection-engine
x-pack/solutions/security/packages/kbn-securitysolution-utils @elastic/security-detection-engine @elastic/security-detection-rule-management
x-pack/solutions/security/packages/navigation @elastic/security-threat-hunting-explore
x-pack/solutions/security/packages/side_nav @elastic/security-threat-hunting-explore
x-pack/solutions/security/packages/storybook/config @elastic/security-threat-hunting-explore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36637,7 +36637,6 @@
"xpack.securitySolution.detectionEngine.ruleDetails.enableRuleLabel": "Activer",
"xpack.securitySolution.detectionEngine.ruleDetails.endpointExceptionsTab": "Exceptions de point de terminaison",
"xpack.securitySolution.detectionEngine.ruleDetails.falsePositivesFieldLabel": "Exemples de faux positifs",
"xpack.securitySolution.detectionEngine.ruleDetails.fromFieldLabel": "Temps de récupération supplémentaire",
"xpack.securitySolution.detectionEngine.ruleDetails.historyWindowSizeFieldLabel": "Taille de la fenêtre d’historique",
"xpack.securitySolution.detectionEngine.ruleDetails.indexFieldLabel": "Modèles d'indexation",
"xpack.securitySolution.detectionEngine.ruleDetails.installAndEnableButtonLabel": "Installer et activer",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36496,7 +36496,6 @@
"xpack.securitySolution.detectionEngine.ruleDetails.enableRuleLabel": "有効にする",
"xpack.securitySolution.detectionEngine.ruleDetails.endpointExceptionsTab": "エンドポイント例外",
"xpack.securitySolution.detectionEngine.ruleDetails.falsePositivesFieldLabel": "誤検出の例",
"xpack.securitySolution.detectionEngine.ruleDetails.fromFieldLabel": "追加のルックバック時間",
"xpack.securitySolution.detectionEngine.ruleDetails.historyWindowSizeFieldLabel": "履歴ウィンドウサイズ",
"xpack.securitySolution.detectionEngine.ruleDetails.indexFieldLabel": "インデックスパターン",
"xpack.securitySolution.detectionEngine.ruleDetails.installAndEnableButtonLabel": "インストールして有効化",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35954,7 +35954,6 @@
"xpack.securitySolution.detectionEngine.ruleDetails.enableRuleLabel": "启用",
"xpack.securitySolution.detectionEngine.ruleDetails.endpointExceptionsTab": "终端例外",
"xpack.securitySolution.detectionEngine.ruleDetails.falsePositivesFieldLabel": "误报示例",
"xpack.securitySolution.detectionEngine.ruleDetails.fromFieldLabel": "更多回查时间",
"xpack.securitySolution.detectionEngine.ruleDetails.historyWindowSizeFieldLabel": "历史记录窗口大小",
"xpack.securitySolution.detectionEngine.ruleDetails.indexFieldLabel": "索引模式",
"xpack.securitySolution.detectionEngine.ruleDetails.installAndEnableButtonLabel": "安装并启用",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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.
*/

export * from './src/date_math';
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
{
"type": "shared-common",
"id": "@kbn/securitysolution-utils",
"owner": [
"@elastic/security-detection-engine"
],
"owner": ["@elastic/security-detection-engine", "@elastic/security-detection-rule-management"],
"group": "security",
"visibility": "private"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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 { calcDateMathDiff } from './calc_date_math_diff';

describe('calcDateMathDiff', () => {
it.each([
['now-62s', 'now-1m', 2000],
['now-122s', 'now-1m', 62000],
['now-660s', 'now-5m', 360000],
['now-6600s', 'now-5m', 6300000],
['now-7500s', 'now-5m', 7200000],
['now-1m', 'now-62s', -2000],
['now-1m', 'now-122s', -62000],
['now-5m', 'now-660s', -360000],
['now-5m', 'now-6600s', -6300000],
['now-5m', 'now-7500s', -7200000],
['now-1s', 'now-1s', 0],
['now-1m', 'now-1m', 0],
['now-1h', 'now-1h', 0],
['now-1d', 'now-1d', 0],
])('calculates milliseconds diff between "%s" and "%s"', (start, end, expected) => {
const result = calcDateMathDiff(start, end);

expect(result).toEqual(expected);
});

test('returns "undefined" when start is invalid date math', () => {
const result = calcDateMathDiff('invalid', 'now-5m');

expect(result).toBeUndefined();
});

test('returns "undefined" when end is invalid date math', () => {
const result = calcDateMathDiff('now-300s', 'invalid');

expect(result).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 dateMath from '@kbn/datemath';

/**
* Calculates difference between date math expressions in milliseconds.
*/
export function calcDateMathDiff(start: string, end: string): number | undefined {
const now = new Date();
const startMoment = dateMath.parse(start, { forceNow: now });
const endMoment = dateMath.parse(end, { forceNow: now });

if (!startMoment || !endMoment) {
return undefined;
}

const result = endMoment.diff(startMoment, 'ms');

return !isNaN(result) ? result : undefined;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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.
*/

export * from './calc_date_math_diff';
export * from './normalize_date_math';
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 { normalizeDateMath } from './normalize_date_math';

describe('normalizeDateMath', () => {
it.each([
['now-60s', 'now-1m'],
['now-60m', 'now-1h'],
['now-24h', 'now-1d'],
['now+60s', 'now+1m'],
['now+60m', 'now+1h'],
['now+24h', 'now+1d'],
])('normalizes %s', (sourceDateMath, normalizedDateMath) => {
const result = normalizeDateMath(sourceDateMath);

expect(result).toBe(normalizedDateMath);
});

it.each([['now'], ['now-invalid'], ['invalid']])('returns %s non-normalized', (dateMath) => {
const result = normalizeDateMath(dateMath);

expect(result).toBe(dateMath);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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 { TimeDuration } from '../time_duration/time_duration';
import { calcDateMathDiff } from './calc_date_math_diff';

/**
* Normalizes date math
*/
export function normalizeDateMath(input: string): string {
try {
const ms = calcDateMathDiff('now', input);

if (ms === undefined || (ms > -1000 && ms < 1000)) {
return input;
}

if (ms === 0) {
return 'now';
}

const offset = TimeDuration.fromMilliseconds(ms);

return offset.value < 0 ? `now${offset}` : `now+${offset}`;
} catch {
return input;
}
}
Loading

0 comments on commit 30bb71a

Please sign in to comment.