kibana/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts
Wafaa Nasr d01a5c4fe0
[Detection Engine][Rule Suppression] Add Suppression to EQL Non-sequence based queries (#176422)
# Summary

- Address adding suppression to EQL rules
https://github.com/elastic/security-team/issues/7773
- Milestone details https://github.com/elastic/security-team/issues/8432

## Checklist
- [x] Functional changes are hidden behind a feature flag. If not
hidden, the PR explains why these changes are being implemented in a
long-living feature branch.
- [x] Functional changes are covered with a test plan and automated
tests. [Test plan](https://github.com/elastic/security-team/pull/9155)
- [ ] Stability of new and changed tests is verified using the [Flaky
Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner) in
both ESS and Serverless. By default, use 200 runs for ESS and 200 runs
for Serverless.
* Cypress ESS:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5686
* Cypress Serverless:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5687
* FTR ESS:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5688
* FTR Serverless:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5689
- [x] Comprehensive manual testing is done by two engineers: the PR
author and one of the PR reviewers. Changes are tested in both ESS and
Serverless.
- [x] Mapping changes are accompanied by a technical design document. It
can be a GitHub issue or an RFC explaining the changes. The design
document is shared with and approved by the appropriate teams and
individual stakeholders.
- [ ] (OPTIONAL) OpenAPI specs changes include detailed descriptions and
examples of usage and are ready to be released on
https://docs.elastic.co/api-reference. NOTE: This is optional because at
the moment we don't have yet any OpenAPI specs that would be fully
"documented" and "GA-ready" for publishing on
https://docs.elastic.co/api-reference.
- [x] Functional changes are communicated to the Docs team. A ticket is
opened in https://github.com/elastic/security-docs using the [`Internal
documentation request (Elastic
employees)`](https://github.com/elastic/security-docs/issues/new?assignees=&labels=&projects=&template=docs-request-internal.yaml&title=%5BRequest%5D+)
template. The following information is included: feature flags used,
target ESS version, planned timing for ESS and Serverless releases.
- [x] Check if in timeline we can show the suppression count column when
the user clicks on investigate on timeline for Eql suppressed Alerts
(https://github.com/elastic/kibana/issues/180976)

## Related Issues
* Sub-PRs
- Address EQL schema changes PR
https://github.com/elastic/kibana/pull/176391
- Adding Feature flag PR and updating the Frontend Part in Rule
Create/Edit https://github.com/elastic/kibana/pull/176398
- Adding Backend changes and FTR tests
https://github.com/elastic/kibana/pull/176597
- Fix Investigate in Timeline for the Suppressed Alerts
https://github.com/elastic/kibana/pull/177839
- Add Cypress e2e tests https://github.com/elastic/kibana/pull/177870
- Disable EQL sequence suppression in the UI and fix Cypress `after`
esArchive path https://github.com/elastic/kibana/pull/178531
- Docs Issue https://github.com/elastic/security-docs/issues/4977
- Test plan https://github.com/elastic/security-team/pull/9155

## Screenshots/recordings

### Non-Sequence Suppression

1. Rule creation, Suppression based on a single value


8d168bce-15d3-45c2-a5dc-238b3ac01626

2. Rule creation, Suppression based on an array of values
  

0e3312a9-4eae-476b-9c1e-c68189bbaf95

3. Investigate In Timeline


e10c8668-4d5b-4748-b8a1-678603b4a8a5


### Disabled Sequence Suppression

1. UI


01faa649-ca8b-43e4-a398-42ab242e7a72

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Ryland Herrick <ryalnd@gmail.com>
Co-authored-by: Georgii Gorbachev <georgii.gorbachev@elastic.co>
2024-04-17 10:38:25 +02:00

350 lines
12 KiB
TypeScript

/*
* 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 {
hasEqlSequenceQuery,
hasNestedEntry,
isThreatMatchRule,
normalizeMachineLearningJobIds,
normalizeThresholdField,
isMlRule,
isEsqlRule,
isSuppressibleAlertRule,
isSuppressionRuleConfiguredWithDuration,
isSuppressionRuleConfiguredWithGroupBy,
isSuppressionRuleConfiguredWithMissingFields,
isEqlSequenceQuery,
} from './utils';
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
import { hasLargeValueList } from '@kbn/securitysolution-list-utils';
import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types';
describe('#hasLargeValueList', () => {
test('it returns false if empty array', () => {
const hasLists = hasLargeValueList([]);
expect(hasLists).toBeFalsy();
});
test('it returns true if item of type EntryList exists', () => {
const entries: EntriesArray = [
{
field: 'actingProcess.file.signer',
type: 'list',
operator: 'included',
list: { id: 'some id', type: 'ip' },
},
{
field: 'file.signature.signer',
type: 'match',
operator: 'excluded',
value: 'Global Signer',
},
];
const hasLists = hasLargeValueList(entries);
expect(hasLists).toBeTruthy();
});
test('it returns false if item of type EntryList does not exist', () => {
const entries: EntriesArray = [
{
field: 'actingProcess.file.signer',
type: 'match',
operator: 'included',
value: 'Elastic, N.V.',
},
{
field: 'file.signature.signer',
type: 'match',
operator: 'excluded',
value: 'Global Signer',
},
];
const hasLists = hasLargeValueList(entries);
expect(hasLists).toBeFalsy();
});
});
describe('#hasNestedEntry', () => {
test('it returns false if empty array', () => {
const hasLists = hasNestedEntry([]);
expect(hasLists).toBeFalsy();
});
test('it returns true if item of type EntryNested exists', () => {
const entries: EntriesArray = [
{
field: 'actingProcess.file.signer',
type: 'nested',
entries: [
{ field: 'some field', type: 'match', operator: 'included', value: 'some value' },
],
},
{
field: 'file.signature.signer',
type: 'match',
operator: 'excluded',
value: 'Global Signer',
},
];
const hasLists = hasNestedEntry(entries);
expect(hasLists).toBeTruthy();
});
test('it returns false if item of type EntryNested does not exist', () => {
const entries: EntriesArray = [
{
field: 'actingProcess.file.signer',
type: 'match',
operator: 'included',
value: 'Elastic, N.V.',
},
{
field: 'file.signature.signer',
type: 'match',
operator: 'excluded',
value: 'Global Signer',
},
];
const hasLists = hasNestedEntry(entries);
expect(hasLists).toBeFalsy();
});
describe('isThreatMatchRule', () => {
test('it returns true if a threat match rule', () => {
expect(isThreatMatchRule('threat_match')).toEqual(true);
});
test('it returns false if not a threat match rule', () => {
expect(isThreatMatchRule('query')).toEqual(false);
});
});
});
describe('isMlRule', () => {
test('it returns true if a ML rule', () => {
expect(isMlRule('machine_learning')).toEqual(true);
});
test('it returns false if not a Ml rule', () => {
expect(isMlRule('query')).toEqual(false);
});
});
describe('isEsqlRule', () => {
test('it returns true if a ES|QL rule', () => {
expect(isEsqlRule('esql')).toEqual(true);
});
test('it returns false if not a ES|QL rule', () => {
expect(isEsqlRule('query')).toEqual(false);
});
});
describe('#hasEqlSequenceQuery', () => {
describe('when a non-sequence query is passed', () => {
const query = 'process where process.name == "regsvr32.exe"';
it('should return false', () => {
expect(hasEqlSequenceQuery(query)).toEqual(false);
});
});
describe('when a sequence query is passed', () => {
const query = 'sequence [process where process.name = "test.exe"]';
it('should return true', () => {
expect(hasEqlSequenceQuery(query)).toEqual(true);
});
});
describe('when a sequence query is passed with extra white space and escape characters', () => {
const query = '\tsequence \n [process where process.name = "test.exe"]';
it('should return true', () => {
expect(hasEqlSequenceQuery(query)).toEqual(true);
});
});
describe('when a non-sequence query is passed using the word sequence', () => {
const query = 'sequence where true';
it('should return false', () => {
expect(hasEqlSequenceQuery(query)).toEqual(false);
});
});
describe('when a non-sequence query is passed using the word sequence with extra white space and escape characters', () => {
const query = ' sequence\nwhere\ttrue';
it('should return false', () => {
expect(hasEqlSequenceQuery(query)).toEqual(false);
});
});
});
describe('normalizeThresholdField', () => {
it('converts a string to a string array', () => {
expect(normalizeThresholdField('host.name')).toEqual(['host.name']);
});
it('returns a string array when a string array is passed in', () => {
expect(normalizeThresholdField(['host.name'])).toEqual(['host.name']);
});
it('converts undefined to an empty array', () => {
expect(normalizeThresholdField(undefined)).toEqual([]);
});
it('converts null to an empty array', () => {
expect(normalizeThresholdField(null)).toEqual([]);
});
it('converts an empty string to an empty array', () => {
expect(normalizeThresholdField('')).toEqual([]);
});
});
describe('normalizeMachineLearningJobIds', () => {
it('converts a string to a string array', () => {
expect(normalizeMachineLearningJobIds('ml_job_id')).toEqual(['ml_job_id']);
});
it('preserves a single-valued array ', () => {
expect(normalizeMachineLearningJobIds(['ml_job_id'])).toEqual(['ml_job_id']);
});
it('preserves a multi-valued array ', () => {
expect(normalizeMachineLearningJobIds(['ml_job_id', 'other_ml_job_id'])).toEqual([
'ml_job_id',
'other_ml_job_id',
]);
});
});
describe('Alert Suppression Rules', () => {
describe('isSuppressibleAlertRule', () => {
test('should return true for a suppressible rule type', () => {
// Rule types that support alert suppression:
expect(isSuppressibleAlertRule('threshold')).toBe(true);
expect(isSuppressibleAlertRule('saved_query')).toBe(true);
expect(isSuppressibleAlertRule('query')).toBe(true);
expect(isSuppressibleAlertRule('threat_match')).toBe(true);
expect(isSuppressibleAlertRule('new_terms')).toBe(true);
expect(isSuppressibleAlertRule('eql')).toBe(true);
// Rule types that don't support alert suppression:
expect(isSuppressibleAlertRule('machine_learning')).toBe(false);
expect(isSuppressibleAlertRule('esql')).toBe(false);
});
test('should return false for an unknown rule type', () => {
const ruleType = '123' as Type;
const result = isSuppressibleAlertRule(ruleType);
expect(result).toBe(false);
});
});
describe('isSuppressionRuleConfiguredWithDuration', () => {
test('should return true for a suppressible rule type', () => {
// Rule types that support alert suppression:
expect(isSuppressionRuleConfiguredWithDuration('threshold')).toBe(true);
expect(isSuppressionRuleConfiguredWithDuration('saved_query')).toBe(true);
expect(isSuppressionRuleConfiguredWithDuration('query')).toBe(true);
expect(isSuppressionRuleConfiguredWithDuration('threat_match')).toBe(true);
expect(isSuppressionRuleConfiguredWithDuration('new_terms')).toBe(true);
expect(isSuppressionRuleConfiguredWithDuration('eql')).toBe(true);
// Rule types that don't support alert suppression:
expect(isSuppressionRuleConfiguredWithDuration('machine_learning')).toBe(false);
expect(isSuppressionRuleConfiguredWithDuration('esql')).toBe(false);
});
test('should return false for an unknown rule type', () => {
const ruleType = '123' as Type;
const result = isSuppressionRuleConfiguredWithDuration(ruleType);
expect(result).toBe(false);
});
});
describe('isSuppressionRuleConfiguredWithGroupBy', () => {
test('should return true for a suppressible rule type with groupBy', () => {
// Rule types that support alert suppression groupBy:
expect(isSuppressionRuleConfiguredWithGroupBy('saved_query')).toBe(true);
expect(isSuppressionRuleConfiguredWithGroupBy('query')).toBe(true);
expect(isSuppressionRuleConfiguredWithGroupBy('threat_match')).toBe(true);
expect(isSuppressionRuleConfiguredWithGroupBy('new_terms')).toBe(true);
expect(isSuppressionRuleConfiguredWithGroupBy('eql')).toBe(true);
// Rule types that don't support alert suppression:
expect(isSuppressionRuleConfiguredWithGroupBy('machine_learning')).toBe(false);
expect(isSuppressionRuleConfiguredWithGroupBy('esql')).toBe(false);
});
test('should return false for a threshold rule type', () => {
const result = isSuppressionRuleConfiguredWithGroupBy('threshold');
expect(result).toBe(false);
});
test('should return false for an unknown rule type', () => {
const ruleType = '123' as Type;
const result = isSuppressionRuleConfiguredWithGroupBy(ruleType);
expect(result).toBe(false);
});
});
describe('isSuppressionRuleConfiguredWithMissingFields', () => {
test('should return true for a suppressible rule type with missing fields', () => {
// Rule types that support alert suppression groupBy:
expect(isSuppressionRuleConfiguredWithMissingFields('saved_query')).toBe(true);
expect(isSuppressionRuleConfiguredWithMissingFields('query')).toBe(true);
expect(isSuppressionRuleConfiguredWithMissingFields('threat_match')).toBe(true);
expect(isSuppressionRuleConfiguredWithMissingFields('new_terms')).toBe(true);
expect(isSuppressionRuleConfiguredWithMissingFields('eql')).toBe(true);
// Rule types that don't support alert suppression:
expect(isSuppressionRuleConfiguredWithMissingFields('machine_learning')).toBe(false);
expect(isSuppressionRuleConfiguredWithMissingFields('esql')).toBe(false);
});
test('should return false for a threshold rule type', () => {
const result = isSuppressionRuleConfiguredWithMissingFields('threshold');
expect(result).toBe(false);
});
test('should return false for an unknown rule type', () => {
const ruleType = '123' as Type;
const result = isSuppressionRuleConfiguredWithMissingFields(ruleType);
expect(result).toBe(false);
});
});
describe('isEqlSequenceQuery', () => {
it('is false if query is undefined', () => {
const result = isEqlSequenceQuery(undefined);
expect(result).toBe(false);
});
it('is false if query is an empty string', () => {
const result = isEqlSequenceQuery('');
expect(result).toBe(false);
});
it('is false if query is an nonempty string', () => {
const result = isEqlSequenceQuery('any where true');
expect(result).toBe(false);
});
it('is true if query begins with "sequence"', () => {
const query = 'sequence where true';
expect(isEqlSequenceQuery(query)).toBe(true);
});
it('is true if query begins with some whitespace and then "sequence"', () => {
const query = ' sequence where true';
expect(isEqlSequenceQuery(query)).toBe(true);
});
});
});