mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 10:23:14 -04:00
# 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 value8d168bce
-15d3-45c2-a5dc-238b3ac01626 2. Rule creation, Suppression based on an array of values0e3312a9
-4eae-476b-9c1e-c68189bbaf95 3. Investigate In Timelinee10c8668
-4d5b-4748-b8a1-678603b4a8a5 ### Disabled Sequence Suppression 1. UI01faa649
-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>
350 lines
12 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|