From 21c7f5e074db54fd1c79f35ef5979b61ef0aa0df Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Mon, 24 Oct 2022 14:56:36 -0700 Subject: [PATCH] [Security Solution][Alerts] Refactor rule execution logic integration tests (#142679) * Separate rule execution logic tests and move bulk of the tests to preview for speed * Remove bad dependency * Update unit test snapshot * Fix flaky test * Fix another flaky test * Fix more imports * Remove superfluous return type --- .buildkite/ftr_configs.yml | 1 + .../signals/executors/threshold.ts | 2 +- .../get_threshold_signal_history.test.ts.snap | 4 +- .../get_threshold_signal_history.test.ts | 8 +- .../threshold/get_threshold_signal_history.ts | 13 +- .../common/config.ts | 1 + .../group1/create_new_terms.ts | 428 +---- .../group1/create_threat_matching.ts | 1421 --------------- .../group1/generating_signals.ts | 1554 ----------------- .../security_and_spaces/group1/index.ts | 3 - .../group1/preview_rules.ts | 4 +- .../rule_execution_logic/README.md | 11 + .../rule_execution_logic/config.ts | 18 + .../rule_execution_logic/eql.ts | 606 +++++++ .../rule_execution_logic/index.ts | 21 + .../machine_learning.ts} | 141 +- .../rule_execution_logic/new_terms.ts | 385 ++++ .../rule_execution_logic/query.ts | 426 +++++ .../rule_execution_logic/saved_query.ts | 85 + .../rule_execution_logic/threat_match.ts | 1306 ++++++++++++++ .../rule_execution_logic/threshold.ts | 385 ++++ .../utils/get_preview_alerts.ts | 45 + .../utils/index.ts | 4 + .../utils/machine_learning_setup.ts | 54 + .../utils/preview_rule.ts | 50 + .../preview_rule_with_exception_entries.ts | 63 + .../alerts/8.0.0/data.json.gz | Bin 9231 -> 9203 bytes .../alerts/8.0.0/mappings.json.gz | Bin 9711 -> 9609 bytes .../alerts/8.1.0/data.json.gz | Bin 9500 -> 9456 bytes .../alerts/8.1.0/mappings.json.gz | Bin 9739 -> 9616 bytes .../tests/basic/search_strategy.ts | 3 +- 31 files changed, 3542 insertions(+), 3500 deletions(-) delete mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_threat_matching.ts delete mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group1/generating_signals.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/README.md create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/config.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/eql.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/index.ts rename x-pack/test/detection_engine_api_integration/security_and_spaces/{group1/create_ml.ts => rule_execution_logic/machine_learning.ts} (69%) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/new_terms.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/saved_query.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threshold.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/get_preview_alerts.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/machine_learning_setup.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/preview_rule.ts create mode 100644 x-pack/test/detection_engine_api_integration/utils/preview_rule_with_exception_entries.ts diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index aa35797d1f986..cc625a09fadd0 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -142,6 +142,7 @@ enabled: - x-pack/test/detection_engine_api_integration/security_and_spaces/group8/config.ts - x-pack/test/detection_engine_api_integration/security_and_spaces/group9/config.ts - x-pack/test/detection_engine_api_integration/security_and_spaces/group10/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/config.ts - x-pack/test/encrypted_saved_objects_api_integration/config.ts - x-pack/test/endpoint_api_integration_no_ingest/config.ts - x-pack/test/examples/config.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts index 7a148e80282dc..515caf5dcd5e1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts @@ -92,7 +92,7 @@ export const thresholdExecutor = async ({ : await getThresholdSignalHistory({ from: tuple.from.toISOString(), to: tuple.to.toISOString(), - ruleId: ruleParams.ruleId, + frameworkRuleId: completeRule.alertId, bucketByFields: ruleParams.threshold.field, ruleDataReader, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/__snapshots__/get_threshold_signal_history.test.ts.snap b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/__snapshots__/get_threshold_signal_history.test.ts.snap index 005ce07afcac4..bb9e29f1f5b52 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/__snapshots__/get_threshold_signal_history.test.ts.snap +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/__snapshots__/get_threshold_signal_history.test.ts.snap @@ -17,7 +17,7 @@ Object { }, Object { "term": Object { - "signal.rule.rule_id": "threshold-rule", + "kibana.alert.rule.uuid": "threshold-rule", }, }, Object { @@ -91,7 +91,7 @@ Object { }, Object { "term": Object { - "signal.rule.rule_id": "threshold-rule", + "kibana.alert.rule.uuid": "threshold-rule", }, }, Object { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.test.ts index d9db2972a89d3..0e896ad49cd82 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.test.ts @@ -12,10 +12,10 @@ describe('buildPreviousThresholdAlertRequest', () => { const bucketByFields: string[] = []; const to = 'now'; const from = 'now-6m'; - const ruleId = 'threshold-rule'; + const frameworkRuleId = 'threshold-rule'; expect( - buildPreviousThresholdAlertRequest({ from, to, ruleId, bucketByFields }) + buildPreviousThresholdAlertRequest({ from, to, frameworkRuleId, bucketByFields }) ).toMatchSnapshot(); }); @@ -23,10 +23,10 @@ describe('buildPreviousThresholdAlertRequest', () => { const bucketByFields: string[] = ['host.name', 'user.name']; const to = 'now'; const from = 'now-6m'; - const ruleId = 'threshold-rule'; + const frameworkRuleId = 'threshold-rule'; expect( - buildPreviousThresholdAlertRequest({ from, to, ruleId, bucketByFields }) + buildPreviousThresholdAlertRequest({ from, to, frameworkRuleId, bucketByFields }) ).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts index 997e6c213f3b1..4826cd574a906 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts @@ -7,6 +7,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { IRuleDataReader } from '@kbn/rule-registry-plugin/server'; +import { ALERT_RULE_UUID } from '@kbn/rule-data-utils'; import type { ThresholdSignalHistory } from '../types'; import { buildThresholdSignalHistory } from './build_signal_history'; import { createErrorsFromShard } from '../utils'; @@ -14,7 +15,7 @@ import { createErrorsFromShard } from '../utils'; interface GetThresholdSignalHistoryParams { from: string; to: string; - ruleId: string; + frameworkRuleId: string; bucketByFields: string[]; ruleDataReader: IRuleDataReader; } @@ -22,7 +23,7 @@ interface GetThresholdSignalHistoryParams { export const getThresholdSignalHistory = async ({ from, to, - ruleId, + frameworkRuleId, bucketByFields, ruleDataReader, }: GetThresholdSignalHistoryParams): Promise<{ @@ -32,7 +33,7 @@ export const getThresholdSignalHistory = async ({ const request = buildPreviousThresholdAlertRequest({ from, to, - ruleId, + frameworkRuleId, bucketByFields, }); @@ -48,12 +49,12 @@ export const getThresholdSignalHistory = async ({ export const buildPreviousThresholdAlertRequest = ({ from, to, - ruleId, + frameworkRuleId, bucketByFields, }: { from: string; to: string; - ruleId: string; + frameworkRuleId: string; bucketByFields: string[]; }): estypes.SearchRequest => { return { @@ -80,7 +81,7 @@ export const buildPreviousThresholdAlertRequest = ({ }, { term: { - 'signal.rule.rule_id': ruleId, + [ALERT_RULE_UUID]: frameworkRuleId, }, }, // We might find a signal that was generated on the interval for old data... make sure to exclude those. diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index a95cb937d4cd9..fbbe7fd62a7a8 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -77,6 +77,7 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s `--xpack.securitySolution.enableExperimental=${JSON.stringify([ 'previewTelemetryUrlEnabled', ])}`, + '--xpack.task_manager.poll_interval=1000', ...(ssl ? [ `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_new_terms.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_new_terms.ts index bfb369e0091b5..095ce3766918d 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_new_terms.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_new_terms.ts @@ -5,78 +5,26 @@ * 2.0. */ -import { orderBy } from 'lodash'; import expect from '@kbn/expect'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; -import { RuleExecutionStatus } from '@kbn/security-solution-plugin/common/detection_engine/rule_monitoring'; -import { NewTermsRuleCreateProps } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; import { getCreateNewTermsRulesSchemaMock } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema/mocks'; -import { DetectionAlert } from '@kbn/security-solution-plugin/common/detection_engine/schemas/alerts'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - createRule, - createRuleWithExceptionEntries, - createSignalsIndex, - deleteAllAlerts, - deleteSignalsIndex, - getOpenSignals, - getSignalsByIds, - waitForRuleSuccessOrStatus, - waitForSignalsToBePresent, -} from '../../utils'; +import { deleteAllAlerts } from '../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { - const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); const log = getService('log'); - const es = getService('es'); /** * Specific api integration tests for threat matching rule type */ describe('create_new_terms', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - beforeEach(async () => { - await createSignalsIndex(supertest, log); - }); - afterEach(async () => { - await deleteSignalsIndex(supertest, log); await deleteAllAlerts(supertest, log); }); - it('should create a single rule with a rule_id and validate it ran successfully', async () => { - const ruleResponse = await createRule( - supertest, - log, - getCreateNewTermsRulesSchemaMock('rule-1', true) - ); - - await waitForRuleSuccessOrStatus( - supertest, - log, - ruleResponse.id, - RuleExecutionStatus.succeeded - ); - - const { body: rule } = await supertest - .get(DETECTION_ENGINE_RULES_URL) - .set('kbn-xsrf', 'true') - .query({ id: ruleResponse.id }) - .expect(200); - - expect(rule?.execution_summary?.last_execution.status).to.eql('succeeded'); - }); - it('should not be able to create a new terms rule with too small history window', async () => { const rule = { ...getCreateNewTermsRulesSchemaMock('rule-1'), @@ -92,379 +40,5 @@ export default ({ getService }: FtrProviderContext) => { "params invalid: History window size is smaller than rule interval + additional lookback, 'historyWindowStart' must be earlier than 'from'" ); }); - - const removeRandomValuedProperties = (alert: DetectionAlert | undefined) => { - if (!alert) { - return undefined; - } - const { - 'kibana.version': version, - 'kibana.alert.rule.execution.uuid': execUuid, - 'kibana.alert.rule.uuid': uuid, - '@timestamp': timestamp, - 'kibana.alert.rule.created_at': createdAt, - 'kibana.alert.rule.updated_at': updatedAt, - 'kibana.alert.uuid': alertUuid, - ...restOfAlert - } = alert; - return restOfAlert; - }; - - // This test also tests that alerts are NOT created for terms that are not new: the host name - // suricata-sensor-san-francisco appears in a document at 2019-02-19T20:42:08.230Z, but also appears - // in earlier documents so is not new. An alert should not be generated for that term. - it('should generate 1 alert with 1 selected field', async () => { - const rule: NewTermsRuleCreateProps = { - ...getCreateNewTermsRulesSchemaMock('rule-1', true), - new_terms_fields: ['host.name'], - from: '2019-02-19T20:42:00.000Z', - history_window_start: '2019-01-19T20:42:00.000Z', - }; - - const createdRule = await createRule(supertest, log, rule); - - await waitForRuleSuccessOrStatus( - supertest, - log, - createdRule.id, - RuleExecutionStatus.succeeded - ); - - const signalsOpen = await getOpenSignals(supertest, log, es, createdRule); - expect(signalsOpen.hits.hits.length).eql(1); - expect(removeRandomValuedProperties(signalsOpen.hits.hits[0]._source)).eql({ - 'kibana.alert.new_terms': ['zeek-newyork-sha-aa8df15'], - 'kibana.alert.rule.category': 'New Terms Rule', - 'kibana.alert.rule.consumer': 'siem', - 'kibana.alert.rule.name': 'Query with a rule id', - 'kibana.alert.rule.producer': 'siem', - 'kibana.alert.rule.rule_type_id': 'siem.newTermsRule', - 'kibana.space_ids': ['default'], - 'kibana.alert.rule.tags': [], - agent: { - ephemeral_id: '7cc2091a-72f1-4c63-843b-fdeb622f9c69', - hostname: 'zeek-newyork-sha-aa8df15', - id: '4b4462ef-93d2-409c-87a6-299d942e5047', - type: 'auditbeat', - version: '8.0.0', - }, - cloud: { instance: { id: '139865230' }, provider: 'digitalocean', region: 'nyc1' }, - ecs: { version: '1.0.0-beta2' }, - host: { - architecture: 'x86_64', - hostname: 'zeek-newyork-sha-aa8df15', - id: '3729d06ce9964aa98549f41cbd99334d', - ip: ['157.230.208.30', '10.10.0.6', 'fe80::24ce:f7ff:fede:a571'], - mac: ['26:ce:f7:de:a5:71'], - name: 'zeek-newyork-sha-aa8df15', - os: { - codename: 'cosmic', - family: 'debian', - kernel: '4.18.0-10-generic', - name: 'Ubuntu', - platform: 'ubuntu', - version: '18.10 (Cosmic Cuttlefish)', - }, - }, - message: - 'Login by user root (UID: 0) on pts/0 (PID: 20638) from 8.42.77.171 (IP: 8.42.77.171)', - process: { pid: 20638 }, - service: { type: 'system' }, - source: { ip: '8.42.77.171' }, - user: { id: 0, name: 'root', terminal: 'pts/0' }, - 'event.action': 'user_login', - 'event.category': 'authentication', - 'event.dataset': 'login', - 'event.kind': 'signal', - 'event.module': 'system', - 'event.origin': '/var/log/wtmp', - 'event.outcome': 'success', - 'event.type': 'authentication_success', - 'kibana.alert.original_time': '2019-02-19T20:42:08.230Z', - 'kibana.alert.ancestors': [ - { - id: 'x07wJ2oB9v5HJNSHhyxi', - type: 'event', - index: 'auditbeat-8.0.0-2019.02.19-000001', - depth: 0, - }, - ], - 'kibana.alert.status': 'active', - 'kibana.alert.workflow_status': 'open', - 'kibana.alert.depth': 1, - 'kibana.alert.reason': - 'authentication event with source 8.42.77.171 by root on zeek-newyork-sha-aa8df15 created high alert Query with a rule id.', - 'kibana.alert.severity': 'high', - 'kibana.alert.risk_score': 55, - 'kibana.alert.rule.parameters': { - description: 'Detecting root and admin users', - risk_score: 55, - severity: 'high', - author: [], - false_positives: [], - from: '2019-02-19T20:42:00.000Z', - rule_id: 'rule-1', - max_signals: 100, - risk_score_mapping: [], - severity_mapping: [], - threat: [], - to: 'now', - references: [], - version: 1, - exceptions_list: [], - immutable: false, - related_integrations: [], - required_fields: [], - setup: '', - type: 'new_terms', - query: '*', - new_terms_fields: ['host.name'], - history_window_start: '2019-01-19T20:42:00.000Z', - index: ['auditbeat-*'], - language: 'kuery', - }, - 'kibana.alert.rule.actions': [], - 'kibana.alert.rule.author': [], - 'kibana.alert.rule.created_by': 'elastic', - 'kibana.alert.rule.description': 'Detecting root and admin users', - 'kibana.alert.rule.enabled': true, - 'kibana.alert.rule.exceptions_list': [], - 'kibana.alert.rule.false_positives': [], - 'kibana.alert.rule.from': '2019-02-19T20:42:00.000Z', - 'kibana.alert.rule.immutable': false, - 'kibana.alert.rule.indices': ['auditbeat-*'], - 'kibana.alert.rule.interval': '5m', - 'kibana.alert.rule.max_signals': 100, - 'kibana.alert.rule.references': [], - 'kibana.alert.rule.risk_score_mapping': [], - 'kibana.alert.rule.rule_id': 'rule-1', - 'kibana.alert.rule.severity_mapping': [], - 'kibana.alert.rule.threat': [], - 'kibana.alert.rule.to': 'now', - 'kibana.alert.rule.type': 'new_terms', - 'kibana.alert.rule.updated_by': 'elastic', - 'kibana.alert.rule.version': 1, - 'kibana.alert.rule.risk_score': 55, - 'kibana.alert.rule.severity': 'high', - 'kibana.alert.original_event.action': 'user_login', - 'kibana.alert.original_event.category': 'authentication', - 'kibana.alert.original_event.dataset': 'login', - 'kibana.alert.original_event.kind': 'event', - 'kibana.alert.original_event.module': 'system', - 'kibana.alert.original_event.origin': '/var/log/wtmp', - 'kibana.alert.original_event.outcome': 'success', - 'kibana.alert.original_event.type': 'authentication_success', - }); - }); - - it('should generate 3 alerts when 1 document has 3 new values', async () => { - const rule: NewTermsRuleCreateProps = { - ...getCreateNewTermsRulesSchemaMock('rule-1', true), - new_terms_fields: ['host.ip'], - from: '2019-02-19T20:42:00.000Z', - history_window_start: '2019-01-19T20:42:00.000Z', - }; - - const createdRule = await createRule(supertest, log, rule); - - await waitForRuleSuccessOrStatus( - supertest, - log, - createdRule.id, - RuleExecutionStatus.succeeded - ); - - const signalsOpen = await getOpenSignals(supertest, log, es, createdRule); - expect(signalsOpen.hits.hits.length).eql(3); - const signalsOrderedByHostIp = orderBy( - signalsOpen.hits.hits, - '_source.kibana.alert.new_terms', - 'asc' - ); - expect(signalsOrderedByHostIp[0]._source?.['kibana.alert.new_terms']).eql(['10.10.0.6']); - expect(signalsOrderedByHostIp[1]._source?.['kibana.alert.new_terms']).eql(['157.230.208.30']); - expect(signalsOrderedByHostIp[2]._source?.['kibana.alert.new_terms']).eql([ - 'fe80::24ce:f7ff:fede:a571', - ]); - }); - - it('should generate alerts for every term when history window is small', async () => { - const rule: NewTermsRuleCreateProps = { - ...getCreateNewTermsRulesSchemaMock('rule-1', true), - new_terms_fields: ['host.name'], - from: '2019-02-19T20:42:00.000Z', - // Set the history_window_start close to 'from' so we should alert on all terms in the time range - history_window_start: '2019-02-19T20:41:59.000Z', - }; - - const createdRule = await createRule(supertest, log, rule); - - await waitForRuleSuccessOrStatus( - supertest, - log, - createdRule.id, - RuleExecutionStatus.succeeded - ); - - const signalsOpen = await getOpenSignals(supertest, log, es, createdRule); - expect(signalsOpen.hits.hits.length).eql(5); - const hostNames = signalsOpen.hits.hits - .map((signal) => signal._source?.['kibana.alert.new_terms']) - .sort(); - expect(hostNames[0]).eql(['suricata-sensor-amsterdam']); - expect(hostNames[1]).eql(['suricata-sensor-san-francisco']); - expect(hostNames[2]).eql(['zeek-newyork-sha-aa8df15']); - expect(hostNames[3]).eql(['zeek-sensor-amsterdam']); - expect(hostNames[4]).eql(['zeek-sensor-san-francisco']); - }); - - describe('timestamp override and fallback', () => { - before(async () => { - await esArchiver.load( - 'x-pack/test/functional/es_archives/security_solution/timestamp_fallback' - ); - await esArchiver.load( - 'x-pack/test/functional/es_archives/security_solution/timestamp_override_3' - ); - }); - after(async () => { - await esArchiver.unload( - 'x-pack/test/functional/es_archives/security_solution/timestamp_fallback' - ); - await esArchiver.unload( - 'x-pack/test/functional/es_archives/security_solution/timestamp_override_3' - ); - }); - - it('should generate the correct alerts', async () => { - const rule: NewTermsRuleCreateProps = { - ...getCreateNewTermsRulesSchemaMock('rule-1', true), - // myfakeindex-3 does not have event.ingested mapped so we can test if the runtime field - // 'kibana.combined_timestamp' handles unmapped fields properly - index: ['timestamp-fallback-test', 'myfakeindex-3'], - new_terms_fields: ['host.name'], - from: '2020-12-16T16:00:00.000Z', - // Set the history_window_start close to 'from' so we should alert on all terms in the time range - history_window_start: '2020-12-16T15:59:00.000Z', - timestamp_override: 'event.ingested', - }; - - const createdRule = await createRule(supertest, log, rule); - - await waitForSignalsToBePresent(supertest, log, 2, [createdRule.id]); - - const signalsOpen = await getSignalsByIds(supertest, log, [createdRule.id]); - expect(signalsOpen.hits.hits.length).eql(2); - const hostNames = signalsOpen.hits.hits - .map((signal) => signal._source?.['kibana.alert.new_terms']) - .sort(); - expect(hostNames[0]).eql(['host-3']); - expect(hostNames[1]).eql(['host-4']); - }); - }); - - it('should apply exceptions', async () => { - const rule: NewTermsRuleCreateProps = { - ...getCreateNewTermsRulesSchemaMock('rule-1', true), - new_terms_fields: ['host.name'], - from: '2019-02-19T20:42:00.000Z', - // Set the history_window_start close to 'from' so we should alert on all terms in the time range - history_window_start: '2019-02-19T20:41:59.000Z', - }; - const createdRule = await createRuleWithExceptionEntries(supertest, log, rule, [ - [ - { - field: 'host.name', - operator: 'included', - type: 'match', - value: 'zeek-sensor-san-francisco', - }, - ], - ]); - - await waitForRuleSuccessOrStatus( - supertest, - log, - createdRule.id, - RuleExecutionStatus.succeeded - ); - - const signalsOpen = await getOpenSignals(supertest, log, es, createdRule); - expect(signalsOpen.hits.hits.length).eql(4); - const hostNames = signalsOpen.hits.hits - .map((signal) => signal._source?.['kibana.alert.new_terms']) - .sort(); - expect(hostNames[0]).eql(['suricata-sensor-amsterdam']); - expect(hostNames[1]).eql(['suricata-sensor-san-francisco']); - expect(hostNames[2]).eql(['zeek-newyork-sha-aa8df15']); - expect(hostNames[3]).eql(['zeek-sensor-amsterdam']); - }); - - it('should work for max signals > 100', async () => { - const maxSignals = 200; - const rule: NewTermsRuleCreateProps = { - ...getCreateNewTermsRulesSchemaMock('rule-1', true), - new_terms_fields: ['process.pid'], - from: '2018-02-19T20:42:00.000Z', - // Set the history_window_start close to 'from' so we should alert on all terms in the time range - history_window_start: '2018-02-19T20:41:59.000Z', - max_signals: maxSignals, - }; - - const createdRule = await createRule(supertest, log, rule); - - await waitForRuleSuccessOrStatus( - supertest, - log, - createdRule.id, - RuleExecutionStatus.succeeded - ); - - const signalsOpen = await getOpenSignals( - supertest, - log, - es, - createdRule, - RuleExecutionStatus.succeeded, - maxSignals - ); - expect(signalsOpen.hits.hits.length).eql(maxSignals); - const processPids = signalsOpen.hits.hits - .map((signal) => signal._source?.['kibana.alert.new_terms']) - .sort(); - expect(processPids[0]).eql([1]); - }); - - describe('alerts should be be enriched', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk'); - }); - - it('should be enriched with host risk score', async () => { - const rule: NewTermsRuleCreateProps = { - ...getCreateNewTermsRulesSchemaMock('rule-1', true), - new_terms_fields: ['host.name'], - from: '2019-02-19T20:42:00.000Z', - history_window_start: '2019-01-19T20:42:00.000Z', - }; - - const createdRule = await createRule(supertest, log, rule); - - await waitForRuleSuccessOrStatus( - supertest, - log, - createdRule.id, - RuleExecutionStatus.succeeded - ); - - const signalsOpen = await getOpenSignals(supertest, log, es, createdRule); - expect(signalsOpen.hits.hits[0]?._source?.host?.risk?.calculated_level).to.eql('Low'); - expect(signalsOpen.hits.hits[0]?._source?.host?.risk?.calculated_score_norm).to.eql(23); - }); - }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_threat_matching.ts deleted file mode 100644 index b18f716d17d4e..0000000000000 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_threat_matching.ts +++ /dev/null @@ -1,1421 +0,0 @@ -/* - * 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 { get, isEqual } from 'lodash'; -import expect from '@kbn/expect'; -import { - ALERT_REASON, - ALERT_RULE_UUID, - ALERT_STATUS, - ALERT_RULE_NAMESPACE, - ALERT_RULE_UPDATED_AT, - ALERT_UUID, - ALERT_WORKFLOW_STATUS, - SPACE_IDS, - VERSION, -} from '@kbn/rule-data-utils'; -import { flattenWithPrefix } from '@kbn/securitysolution-rules'; - -import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; -import { RuleExecutionStatus } from '@kbn/security-solution-plugin/common/detection_engine/rule_monitoring'; -import { RuleCreateProps } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; -import { - getCreateThreatMatchRulesSchemaMock, - getThreatMatchingSchemaPartialMock, -} from '@kbn/security-solution-plugin/common/detection_engine/rule_schema/mocks'; -import { ENRICHMENT_TYPES } from '@kbn/security-solution-plugin/common/cti/constants'; -import { Ancestor } from '@kbn/security-solution-plugin/server/lib/detection_engine/signals/types'; -import { - ALERT_ANCESTORS, - ALERT_DEPTH, - ALERT_ORIGINAL_EVENT_ACTION, - ALERT_ORIGINAL_EVENT_CATEGORY, - ALERT_ORIGINAL_EVENT_MODULE, - ALERT_ORIGINAL_TIME, -} from '@kbn/security-solution-plugin/common/field_maps/field_names'; -import { - createRule, - createSignalsIndex, - deleteAllAlerts, - deleteSignalsIndex, - getSignalsByIds, - removeServerGeneratedProperties, - waitForRuleSuccessOrStatus, - waitForSignalsToBePresent, -} from '../../utils'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -const format = (value: unknown): string => JSON.stringify(value, null, 2); - -// Asserts that each expected value is included in the subject, independent of -// ordering. Uses _.isEqual for value comparison. -const assertContains = (subject: unknown[], expected: unknown[]) => - expected.forEach((expectedValue) => - expect(subject.some((value) => isEqual(value, expectedValue))).to.eql( - true, - `expected ${format(subject)} to contain ${format(expectedValue)}` - ) - ); - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext) => { - const esArchiver = getService('esArchiver'); - const supertest = getService('supertest'); - const log = getService('log'); - - /** - * Specific api integration tests for threat matching rule type - */ - describe('create_threat_matching', () => { - describe('creating threat match rule', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - beforeEach(async () => { - await createSignalsIndex(supertest, log); - }); - - afterEach(async () => { - await deleteSignalsIndex(supertest, log); - await deleteAllAlerts(supertest, log); - }); - - it('should create a single rule with a rule_id', async () => { - const ruleResponse = await createRule( - supertest, - log, - getCreateThreatMatchRulesSchemaMock() - ); - const bodyToCompare = removeServerGeneratedProperties(ruleResponse); - expect(bodyToCompare).to.eql(getThreatMatchingSchemaPartialMock()); - }); - - it('should create a single rule with a rule_id and validate it ran successfully', async () => { - const ruleResponse = await createRule( - supertest, - log, - getCreateThreatMatchRulesSchemaMock('rule-1', true) - ); - - await waitForRuleSuccessOrStatus( - supertest, - log, - ruleResponse.id, - RuleExecutionStatus.succeeded - ); - - const { body: rule } = await supertest - .get(DETECTION_ENGINE_RULES_URL) - .set('kbn-xsrf', 'true') - .query({ id: ruleResponse.id }) - .expect(200); - - const bodyToCompare = removeServerGeneratedProperties(ruleResponse); - expect(bodyToCompare).to.eql(getThreatMatchingSchemaPartialMock(true)); - - // TODO: https://github.com/elastic/kibana/pull/121644 clean up, make type-safe - expect(rule?.execution_summary?.last_execution.status).to.eql('succeeded'); - }); - }); - - describe('tests with auditbeat data', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - beforeEach(async () => { - await deleteAllAlerts(supertest, log); - await createSignalsIndex(supertest, log); - }); - - afterEach(async () => { - await deleteSignalsIndex(supertest, log); - await deleteAllAlerts(supertest, log); - }); - - it('should be able to execute and get 10 signals when doing a specific query', async () => { - const rule: RuleCreateProps = { - description: 'Detecting root and admin users', - name: 'Query with a rule id', - severity: 'high', - index: ['auditbeat-*'], - type: 'threat_match', - risk_score: 55, - language: 'kuery', - rule_id: 'rule-1', - from: '1900-01-01T00:00:00.000Z', - query: '*:*', - threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip - threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity - threat_mapping: [ - // We match host.name against host.name - { - entries: [ - { - field: 'host.name', - value: 'host.name', - type: 'mapping', - }, - ], - }, - ], - threat_filters: [], - }; - - const createdRule = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, createdRule.id); - await waitForSignalsToBePresent(supertest, log, 10, [createdRule.id]); - const signalsOpen = await getSignalsByIds(supertest, log, [createdRule.id]); - expect(signalsOpen.hits.hits.length).equal(10); - const fullSource = signalsOpen.hits.hits.find( - (signal) => - (signal._source?.[ALERT_ANCESTORS] as Ancestor[])[0].id === '7yJ-B2kBR346wHgnhlMn' - ); - const fullSignal = fullSource?._source; - if (!fullSignal) { - return expect(fullSignal).to.be.ok(); - } - expect(fullSignal).eql({ - ...fullSignal, - '@timestamp': fullSignal['@timestamp'], - agent: { - ephemeral_id: '1b4978a0-48be-49b1-ac96-323425b389ab', - hostname: 'zeek-sensor-amsterdam', - id: 'e52588e6-7aa3-4c89-a2c4-d6bc5c286db1', - type: 'auditbeat', - version: '8.0.0', - }, - auditd: { - data: { - hostname: '46.101.47.213', - op: 'PAM:bad_ident', - terminal: 'ssh', - }, - message_type: 'user_err', - result: 'fail', - sequence: 2267, - session: 'unset', - summary: { - actor: { - primary: 'unset', - secondary: 'root', - }, - how: '/usr/sbin/sshd', - object: { - primary: 'ssh', - secondary: '46.101.47.213', - type: 'user-session', - }, - }, - }, - cloud: { - instance: { - id: '133551048', - }, - provider: 'digitalocean', - region: 'ams3', - }, - ecs: { - version: '1.0.0-beta2', - }, - ...flattenWithPrefix('event', { - action: 'error', - category: 'user-login', - module: 'auditd', - kind: 'signal', - }), - host: { - architecture: 'x86_64', - containerized: false, - hostname: 'zeek-sensor-amsterdam', - id: '2ce8b1e7d69e4a1d9c6bcddc473da9d9', - name: 'zeek-sensor-amsterdam', - os: { - codename: 'bionic', - family: 'debian', - kernel: '4.15.0-45-generic', - name: 'Ubuntu', - platform: 'ubuntu', - version: '18.04.2 LTS (Bionic Beaver)', - }, - }, - network: { - direction: 'incoming', - }, - process: { - executable: '/usr/sbin/sshd', - pid: 32739, - }, - service: { - type: 'auditd', - }, - source: { - ip: '46.101.47.213', - }, - user: { - audit: { - id: 'unset', - }, - id: '0', - name: 'root', - }, - [ALERT_ANCESTORS]: [ - { - id: '7yJ-B2kBR346wHgnhlMn', - type: 'event', - index: 'auditbeat-8.0.0-2019.02.19-000001', - depth: 0, - }, - ], - [ALERT_DEPTH]: 1, - [ALERT_ORIGINAL_EVENT_ACTION]: 'error', - [ALERT_ORIGINAL_EVENT_CATEGORY]: 'user-login', - [ALERT_ORIGINAL_EVENT_MODULE]: 'auditd', - [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], - [ALERT_REASON]: - 'user-login event with source 46.101.47.213 by root on zeek-sensor-amsterdam created high alert Query with a rule id.', - [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], - [ALERT_STATUS]: 'active', - [ALERT_UUID]: fullSignal[ALERT_UUID], - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['default'], - [VERSION]: fullSignal[VERSION], - threat: { - enrichments: get(fullSignal, 'threat.enrichments'), - }, - ...flattenWithPrefix(ALERT_RULE_NAMESPACE, { - actions: [], - author: [], - category: 'Indicator Match Rule', - consumer: 'siem', - created_at: createdRule.created_at, - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - exceptions_list: [], - false_positives: [], - from: '1900-01-01T00:00:00.000Z', - immutable: false, - interval: '5m', - max_signals: 100, - name: 'Query with a rule id', - producer: 'siem', - references: [], - risk_score: 55, - risk_score_mapping: [], - rule_id: createdRule.rule_id, - rule_type_id: 'siem.indicatorRule', - severity: 'high', - severity_mapping: [], - tags: [], - threat: [], - to: 'now', - type: 'threat_match', - updated_at: fullSignal[ALERT_RULE_UPDATED_AT], - updated_by: 'elastic', - uuid: createdRule.id, - version: 1, - }), - }); - }); - - it('should return 0 matches if the mapping does not match against anything in the mapping', async () => { - const rule: RuleCreateProps = { - description: 'Detecting root and admin users', - name: 'Query with a rule id', - severity: 'high', - index: ['auditbeat-*'], - type: 'threat_match', - risk_score: 55, - language: 'kuery', - rule_id: 'rule-1', - from: '1900-01-01T00:00:00.000Z', - query: '*:*', - threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip - threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity - threat_mapping: [ - // We match host.name against host.name - { - entries: [ - { - field: 'host.name', - value: 'invalid.mapping.value', // invalid mapping value - type: 'mapping', - }, - ], - }, - ], - threat_filters: [], - }; - - const ruleResponse = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, ruleResponse.id); - const signalsOpen = await getSignalsByIds(supertest, log, [ruleResponse.id]); - expect(signalsOpen.hits.hits.length).equal(0); - }); - - it('should return 0 signals when using an AND and one of the clauses does not have data', async () => { - const rule: RuleCreateProps = { - description: 'Detecting root and admin users', - name: 'Query with a rule id', - severity: 'high', - index: ['auditbeat-*'], - type: 'threat_match', - risk_score: 55, - language: 'kuery', - rule_id: 'rule-1', - from: '1900-01-01T00:00:00.000Z', - query: '*:*', - threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip - threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity - threat_mapping: [ - { - entries: [ - { - field: 'source.ip', - value: 'source.ip', - type: 'mapping', - }, - { - field: 'source.ip', - value: 'destination.ip', // All records from the threat query do NOT have destination.ip, so those records that do not should drop this entire AND clause. - type: 'mapping', - }, - ], - }, - ], - threat_filters: [], - }; - - const ruleResponse = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, ruleResponse.id); - const signalsOpen = await getSignalsByIds(supertest, log, [ruleResponse.id]); - expect(signalsOpen.hits.hits.length).equal(0); - }); - - it('should return 0 signals when using an AND and one of the clauses has a made up value that does not exist', async () => { - const rule: RuleCreateProps = { - description: 'Detecting root and admin users', - name: 'Query with a rule id', - severity: 'high', - type: 'threat_match', - index: ['auditbeat-*'], - risk_score: 55, - language: 'kuery', - rule_id: 'rule-1', - from: '1900-01-01T00:00:00.000Z', - query: '*:*', - threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip - threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity - threat_mapping: [ - { - entries: [ - { - field: 'source.ip', - value: 'source.ip', - type: 'mapping', - }, - { - field: 'source.ip', - value: 'made.up.non.existent.field', // made up field should not match - type: 'mapping', - }, - ], - }, - ], - threat_filters: [], - }; - - const ruleResponse = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, ruleResponse.id); - const signalsOpen = await getSignalsByIds(supertest, log, [ruleResponse.id]); - expect(signalsOpen.hits.hits.length).equal(0); - }); - - describe('timeout behavior', () => { - // Flaky - it.skip('will return an error if a rule execution exceeds the rule interval', async () => { - const rule: RuleCreateProps = { - description: 'Detecting root and admin users', - name: 'Query with a short interval', - severity: 'high', - index: ['auditbeat-*'], - type: 'threat_match', - risk_score: 55, - language: 'kuery', - rule_id: 'rule-1', - from: '1900-01-01T00:00:00.000Z', - query: '*:*', - threat_query: '*:*', // broad query to take more time - threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity - threat_mapping: [ - { - entries: [ - { - field: 'host.name', - value: 'host.name', - type: 'mapping', - }, - ], - }, - ], - threat_filters: [], - concurrent_searches: 1, - interval: '1s', // short interval - items_per_search: 1, // iterate only 1 threat item per loop to ensure we're slow - }; - - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id, RuleExecutionStatus.failed); - - const { body } = await supertest - .post(DETECTION_ENGINE_RULES_URL) - .set('kbn-xsrf', 'true') - .query({ id }) - .expect(200); - - // TODO: https://github.com/elastic/kibana/pull/121644 clean up, make type-safe - expect(body?.execution_summary?.last_execution.message).to.contain( - 'execution has exceeded its allotted interval' - ); - }); - }); - - describe('indicator enrichment: threat-first search', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/filebeat/threat_intel'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/filebeat/threat_intel'); - }); - - it('enriches signals with the single indicator that matched', async () => { - const rule: RuleCreateProps = { - description: 'Detecting root and admin users', - name: 'Query with a rule id', - severity: 'high', - index: ['auditbeat-*'], - type: 'threat_match', - risk_score: 55, - language: 'kuery', - rule_id: 'rule-1', - from: '1900-01-01T00:00:00.000Z', - query: '*:*', // narrow events down to 2 with a destination.ip - threat_indicator_path: 'threat.indicator', - threat_query: 'threat.indicator.domain: 159.89.119.67', // narrow things down to indicators with a domain - threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module - threat_mapping: [ - { - entries: [ - { - value: 'threat.indicator.domain', - field: 'destination.ip', - type: 'mapping', - }, - ], - }, - ], - threat_filters: [], - }; - - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 2, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits.length).equal(2); - - const { hits } = signalsOpen.hits; - const threats = hits.map((hit) => hit._source?.threat); - expect(threats).to.eql([ - { - enrichments: [ - { - feed: {}, - indicator: { - description: "domain should match the auditbeat hosts' data's source.ip", - domain: '159.89.119.67', - first_seen: '2021-01-26T11:09:04.000Z', - provider: 'geenensp', - url: { - full: 'http://159.89.119.67:59600/bin.sh', - scheme: 'http', - }, - type: 'url', - }, - matched: { - atomic: '159.89.119.67', - id: '978783', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'destination.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - ], - }, - { - enrichments: [ - { - feed: {}, - indicator: { - description: "domain should match the auditbeat hosts' data's source.ip", - domain: '159.89.119.67', - first_seen: '2021-01-26T11:09:04.000Z', - provider: 'geenensp', - url: { - full: 'http://159.89.119.67:59600/bin.sh', - scheme: 'http', - }, - type: 'url', - }, - matched: { - atomic: '159.89.119.67', - id: '978783', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'destination.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - ], - }, - ]); - }); - - it('enriches signals with multiple indicators if several matched', async () => { - const rule: RuleCreateProps = { - description: 'Detecting root and admin users', - name: 'Query with a rule id', - severity: 'high', - index: ['auditbeat-*'], - type: 'threat_match', - risk_score: 55, - language: 'kuery', - rule_id: 'rule-1', - from: '1900-01-01T00:00:00.000Z', - query: 'NOT source.port:35326', // specify query to have signals more than treat indicators, but only 1 will match - threat_indicator_path: 'threat.indicator', - threat_query: 'threat.indicator.ip: *', - threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module - threat_mapping: [ - { - entries: [ - { - value: 'threat.indicator.ip', - field: 'source.ip', - type: 'mapping', - }, - ], - }, - ], - threat_filters: [], - }; - - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits.length).equal(1); - - const { hits } = signalsOpen.hits; - const [threat] = hits.map((hit) => hit._source?.threat) as Array<{ - enrichments: unknown[]; - }>; - - assertContains(threat.enrichments, [ - { - feed: {}, - indicator: { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - port: 57324, - provider: 'geenensp', - type: 'url', - }, - matched: { - atomic: '45.115.45.3', - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - { - feed: {}, - indicator: { - description: 'this should match auditbeat/hosts on ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - provider: 'other_provider', - type: 'ip', - }, - - matched: { - atomic: '45.115.45.3', - id: '978787', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - ]); - }); - - it('adds a single indicator that matched multiple fields', async () => { - const rule: RuleCreateProps = { - description: 'Detecting root and admin users', - name: 'Query with a rule id', - severity: 'high', - index: ['auditbeat-*'], - type: 'threat_match', - risk_score: 55, - language: 'kuery', - rule_id: 'rule-1', - from: '1900-01-01T00:00:00.000Z', - query: 'NOT source.port:35326', // specify query to have signals more than treat indicators, but only 1 will match - threat_indicator_path: 'threat.indicator', - threat_query: 'threat.indicator.port: 57324 or threat.indicator.ip:45.115.45.3', // narrow our query to a single indicator - threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module - threat_mapping: [ - { - entries: [ - { - value: 'threat.indicator.port', - field: 'source.port', - type: 'mapping', - }, - ], - }, - { - entries: [ - { - value: 'threat.indicator.ip', - field: 'source.ip', - type: 'mapping', - }, - ], - }, - ], - threat_filters: [], - }; - - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits.length).equal(1); - - const { hits } = signalsOpen.hits; - const [threat] = hits.map((hit) => hit._source?.threat) as Array<{ - enrichments: unknown[]; - }>; - - assertContains(threat.enrichments, [ - { - feed: {}, - indicator: { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - port: 57324, - provider: 'geenensp', - type: 'url', - }, - matched: { - atomic: '45.115.45.3', - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - // We do not merge matched indicators during enrichment, so in - // certain circumstances a given indicator document could appear - // multiple times in an enriched alert (albeit with different - // threat.indicator.matched data). That's the case with the - // first and third indicators matched, here. - { - feed: {}, - indicator: { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - port: 57324, - provider: 'geenensp', - type: 'url', - }, - - matched: { - atomic: 57324, - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.port', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - { - feed: {}, - indicator: { - description: 'this should match auditbeat/hosts on ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - provider: 'other_provider', - type: 'ip', - }, - matched: { - atomic: '45.115.45.3', - id: '978787', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - ]); - }); - - it('generates multiple signals with multiple matches', async () => { - const rule: RuleCreateProps = { - description: 'Detecting root and admin users', - name: 'Query with a rule id', - severity: 'high', - index: ['auditbeat-*'], - type: 'threat_match', - risk_score: 55, - language: 'kuery', - threat_language: 'kuery', - rule_id: 'rule-1', - from: '1900-01-01T00:00:00.000Z', - query: '*:*', // narrow our query to a single record that matches two indicators - threat_indicator_path: 'threat.indicator', - threat_query: '*:*', - threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module - threat_mapping: [ - { - entries: [ - { - value: 'threat.indicator.port', - field: 'source.port', - type: 'mapping', - }, - { - value: 'threat.indicator.ip', - field: 'source.ip', - type: 'mapping', - }, - ], - }, - { - entries: [ - { - value: 'threat.indicator.domain', - field: 'destination.ip', - type: 'mapping', - }, - ], - }, - ], - threat_filters: [], - }; - - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 2, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits.length).equal(2); - - const { hits } = signalsOpen.hits; - const threats = hits.map((hit) => hit._source?.threat) as Array<{ - enrichments: unknown[]; - }>; - - assertContains(threats[0].enrichments, [ - { - feed: {}, - indicator: { - description: "domain should match the auditbeat hosts' data's source.ip", - domain: '159.89.119.67', - first_seen: '2021-01-26T11:09:04.000Z', - provider: 'geenensp', - type: 'url', - url: { - full: 'http://159.89.119.67:59600/bin.sh', - scheme: 'http', - }, - }, - matched: { - atomic: '159.89.119.67', - id: '978783', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'destination.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - { - feed: {}, - indicator: { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - port: 57324, - provider: 'geenensp', - type: 'url', - }, - matched: { - atomic: '45.115.45.3', - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - { - feed: {}, - indicator: { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - port: 57324, - provider: 'geenensp', - type: 'url', - }, - matched: { - atomic: 57324, - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.port', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - ]); - - assertContains(threats[1].enrichments, [ - { - feed: {}, - indicator: { - description: "domain should match the auditbeat hosts' data's source.ip", - domain: '159.89.119.67', - first_seen: '2021-01-26T11:09:04.000Z', - provider: 'geenensp', - type: 'url', - url: { - full: 'http://159.89.119.67:59600/bin.sh', - scheme: 'http', - }, - }, - matched: { - atomic: '159.89.119.67', - id: '978783', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'destination.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - ]); - }); - }); - - describe('indicator enrichment: event-first search', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/filebeat/threat_intel'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/filebeat/threat_intel'); - }); - - it('enriches signals with the single indicator that matched', async () => { - const rule: RuleCreateProps = { - description: 'Detecting root and admin users', - name: 'Query with a rule id', - severity: 'high', - index: ['auditbeat-*'], - type: 'threat_match', - risk_score: 55, - language: 'kuery', - rule_id: 'rule-1', - from: '1900-01-01T00:00:00.000Z', - query: 'destination.ip:159.89.119.67', - threat_indicator_path: 'threat.indicator', - threat_query: 'threat.indicator.domain: *', // narrow things down to indicators with a domain - threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module - threat_mapping: [ - { - entries: [ - { - value: 'threat.indicator.domain', - field: 'destination.ip', - type: 'mapping', - }, - ], - }, - ], - threat_filters: [], - }; - - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 2, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits.length).equal(2); - - const { hits } = signalsOpen.hits; - const threats = hits.map((hit) => hit._source?.threat); - expect(threats).to.eql([ - { - enrichments: [ - { - feed: {}, - indicator: { - description: "domain should match the auditbeat hosts' data's source.ip", - domain: '159.89.119.67', - first_seen: '2021-01-26T11:09:04.000Z', - provider: 'geenensp', - url: { - full: 'http://159.89.119.67:59600/bin.sh', - scheme: 'http', - }, - type: 'url', - }, - matched: { - atomic: '159.89.119.67', - id: '978783', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'destination.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - ], - }, - { - enrichments: [ - { - feed: {}, - indicator: { - description: "domain should match the auditbeat hosts' data's source.ip", - domain: '159.89.119.67', - first_seen: '2021-01-26T11:09:04.000Z', - provider: 'geenensp', - url: { - full: 'http://159.89.119.67:59600/bin.sh', - scheme: 'http', - }, - type: 'url', - }, - matched: { - atomic: '159.89.119.67', - id: '978783', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'destination.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - ], - }, - ]); - }); - - it('enriches signals with multiple indicators if several matched', async () => { - const rule: RuleCreateProps = { - description: 'Detecting root and admin users', - name: 'Query with a rule id', - severity: 'high', - index: ['auditbeat-*'], - type: 'threat_match', - risk_score: 55, - language: 'kuery', - rule_id: 'rule-1', - from: '1900-01-01T00:00:00.000Z', - query: 'source.port: 57324', // narrow our query to a single record that matches two indicators - threat_indicator_path: 'threat.indicator', - threat_query: 'threat.indicator.ip: *', - threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module - threat_mapping: [ - { - entries: [ - { - value: 'threat.indicator.ip', - field: 'source.ip', - type: 'mapping', - }, - ], - }, - ], - threat_filters: [], - }; - - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits.length).equal(1); - - const { hits } = signalsOpen.hits; - const [threat] = hits.map((hit) => hit._source?.threat) as Array<{ - enrichments: unknown[]; - }>; - - assertContains(threat.enrichments, [ - { - feed: {}, - indicator: { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - port: 57324, - provider: 'geenensp', - type: 'url', - }, - matched: { - atomic: '45.115.45.3', - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - { - feed: {}, - indicator: { - description: 'this should match auditbeat/hosts on ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - provider: 'other_provider', - type: 'ip', - }, - - matched: { - atomic: '45.115.45.3', - id: '978787', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - ]); - }); - - it('adds a single indicator that matched multiple fields', async () => { - const rule: RuleCreateProps = { - description: 'Detecting root and admin users', - name: 'Query with a rule id', - severity: 'high', - index: ['auditbeat-*'], - type: 'threat_match', - risk_score: 55, - language: 'kuery', - rule_id: 'rule-1', - from: '1900-01-01T00:00:00.000Z', - query: 'source.port: 57324', // narrow our query to a single record that matches two indicators - threat_indicator_path: 'threat.indicator', - threat_query: 'threat.indicator.ip: *', - threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module - threat_mapping: [ - { - entries: [ - { - value: 'threat.indicator.port', - field: 'source.port', - type: 'mapping', - }, - ], - }, - { - entries: [ - { - value: 'threat.indicator.ip', - field: 'source.ip', - type: 'mapping', - }, - ], - }, - ], - threat_filters: [], - }; - - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits.length).equal(1); - - const { hits } = signalsOpen.hits; - const [threat] = hits.map((hit) => hit._source?.threat) as Array<{ - enrichments: unknown[]; - }>; - - assertContains(threat.enrichments, [ - { - feed: {}, - indicator: { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - port: 57324, - provider: 'geenensp', - type: 'url', - }, - matched: { - atomic: '45.115.45.3', - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - // We do not merge matched indicators during enrichment, so in - // certain circumstances a given indicator document could appear - // multiple times in an enriched alert (albeit with different - // threat.indicator.matched data). That's the case with the - // first and third indicators matched, here. - { - feed: {}, - indicator: { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - port: 57324, - provider: 'geenensp', - type: 'url', - }, - - matched: { - atomic: 57324, - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.port', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - { - feed: {}, - indicator: { - description: 'this should match auditbeat/hosts on ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - provider: 'other_provider', - type: 'ip', - }, - matched: { - atomic: '45.115.45.3', - id: '978787', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - ]); - }); - - it('generates multiple signals with multiple matches', async () => { - const rule: RuleCreateProps = { - description: 'Detecting root and admin users', - name: 'Query with a rule id', - severity: 'high', - index: ['auditbeat-*'], - type: 'threat_match', - risk_score: 55, - language: 'kuery', - threat_language: 'kuery', - rule_id: 'rule-1', - from: '1900-01-01T00:00:00.000Z', - query: '(source.port:57324 and source.ip:45.115.45.3) or destination.ip:159.89.119.67', // narrow our query to a single record that matches two indicators - threat_indicator_path: 'threat.indicator', - threat_query: '*:*', - threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module - threat_mapping: [ - { - entries: [ - { - value: 'threat.indicator.port', - field: 'source.port', - type: 'mapping', - }, - { - value: 'threat.indicator.ip', - field: 'source.ip', - type: 'mapping', - }, - ], - }, - { - entries: [ - { - value: 'threat.indicator.domain', - field: 'destination.ip', - type: 'mapping', - }, - ], - }, - ], - threat_filters: [], - }; - - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 2, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits.length).equal(2); - - const { hits } = signalsOpen.hits; - const threats = hits.map((hit) => hit._source?.threat) as Array<{ - enrichments: unknown[]; - }>; - - assertContains(threats[0].enrichments, [ - { - feed: {}, - indicator: { - description: "domain should match the auditbeat hosts' data's source.ip", - domain: '159.89.119.67', - first_seen: '2021-01-26T11:09:04.000Z', - provider: 'geenensp', - type: 'url', - url: { - full: 'http://159.89.119.67:59600/bin.sh', - scheme: 'http', - }, - }, - matched: { - atomic: '159.89.119.67', - id: '978783', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'destination.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - { - feed: {}, - indicator: { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - port: 57324, - provider: 'geenensp', - type: 'url', - }, - matched: { - atomic: '45.115.45.3', - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - { - feed: {}, - indicator: { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - port: 57324, - provider: 'geenensp', - type: 'url', - }, - matched: { - atomic: 57324, - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.port', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - ]); - - assertContains(threats[1].enrichments, [ - { - feed: {}, - indicator: { - description: "domain should match the auditbeat hosts' data's source.ip", - domain: '159.89.119.67', - first_seen: '2021-01-26T11:09:04.000Z', - provider: 'geenensp', - type: 'url', - url: { - full: 'http://159.89.119.67:59600/bin.sh', - scheme: 'http', - }, - }, - matched: { - atomic: '159.89.119.67', - id: '978783', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'destination.ip', - type: ENRICHMENT_TYPES.IndicatorMatchRule, - }, - }, - ]); - }); - }); - - describe('alerts should be be enriched', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk'); - }); - - it('should be enriched with host risk score', async () => { - const rule: RuleCreateProps = { - description: 'Detecting root and admin users', - name: 'Query with a rule id', - severity: 'high', - index: ['auditbeat-*'], - type: 'threat_match', - risk_score: 55, - language: 'kuery', - rule_id: 'rule-1', - from: '1900-01-01T00:00:00.000Z', - query: '*:*', - threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip - threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity - threat_mapping: [ - // We match host.name against host.name - { - entries: [ - { - field: 'host.name', - value: 'host.name', - type: 'mapping', - }, - ], - }, - ], - threat_filters: [], - }; - - const createdRule = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, createdRule.id); - await waitForSignalsToBePresent(supertest, log, 10, [createdRule.id]); - const signalsOpen = await getSignalsByIds(supertest, log, [createdRule.id]); - expect(signalsOpen.hits.hits.length).equal(10); - const fullSource = signalsOpen.hits.hits.find( - (signal) => - (signal._source?.[ALERT_ANCESTORS] as Ancestor[])[0].id === '7yJ-B2kBR346wHgnhlMn' - ); - const fullSignal = fullSource?._source; - if (!fullSignal) { - return expect(fullSignal).to.be.ok(); - } - - expect(fullSignal?.host?.risk?.calculated_level).to.eql('Critical'); - expect(fullSignal?.host?.risk?.calculated_score_norm).to.eql(70); - }); - }); - }); - }); -}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/generating_signals.ts deleted file mode 100644 index 60e4cef77c896..0000000000000 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/generating_signals.ts +++ /dev/null @@ -1,1554 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; -import { - ALERT_REASON, - ALERT_RISK_SCORE, - ALERT_RULE_NAME, - ALERT_RULE_PARAMETERS, - ALERT_RULE_RULE_ID, - ALERT_RULE_RULE_NAME_OVERRIDE, - ALERT_RULE_UUID, - ALERT_SEVERITY, - ALERT_WORKFLOW_STATUS, - EVENT_ACTION, - EVENT_KIND, -} from '@kbn/rule-data-utils'; -import { flattenWithPrefix } from '@kbn/securitysolution-rules'; - -import { orderBy, get } from 'lodash'; - -import { RuleExecutionStatus } from '@kbn/security-solution-plugin/common/detection_engine/rule_monitoring'; -import { - EqlRuleCreateProps, - QueryRuleCreateProps, - SavedQueryRuleCreateProps, - ThresholdRuleCreateProps, -} from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; -import { Ancestor } from '@kbn/security-solution-plugin/server/lib/detection_engine/signals/types'; -import { - ALERT_ANCESTORS, - ALERT_DEPTH, - ALERT_ORIGINAL_TIME, - ALERT_ORIGINAL_EVENT, - ALERT_ORIGINAL_EVENT_CATEGORY, - ALERT_GROUP_ID, - ALERT_THRESHOLD_RESULT, -} from '@kbn/security-solution-plugin/common/field_maps/field_names'; -import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; -import { - createRule, - createSignalsIndex, - deleteAllAlerts, - deleteSignalsIndex, - getEqlRuleForSignalTesting, - getOpenSignals, - getRuleForSignalTesting, - getSignalsByIds, - getSignalsByRuleIds, - getSimpleRule, - getThresholdRuleForSignalTesting, - waitForRuleSuccessOrStatus, - waitForSignalsToBePresent, -} from '../../utils'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -/** - * Specific _id to use for some of the tests. If the archiver changes and you see errors - * here, update this to a new value of a chosen auditbeat record and update the tests values. - */ -export const ID = 'BhbXBmkBR346wHgn4PeZ'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext) => { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - const es = getService('es'); - const log = getService('log'); - - describe('Generating signals from source indexes', () => { - beforeEach(async () => { - await deleteSignalsIndex(supertest, log); - await createSignalsIndex(supertest, log); - }); - - afterEach(async () => { - await deleteSignalsIndex(supertest, log); - await deleteAllAlerts(supertest, log); - }); - - describe('Signals from audit beat are of the expected structure', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - it('should have the specific audit record for _id or none of these tests below will pass', async () => { - const rule: QueryRuleCreateProps = { - ...getRuleForSignalTesting(['auditbeat-*']), - query: `_id:${ID}`, - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits.length).greaterThan(0); - }); - - it('should abide by max_signals > 100', async () => { - const maxSignals = 500; - const rule: QueryRuleCreateProps = { - ...getRuleForSignalTesting(['auditbeat-*']), - max_signals: maxSignals, - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, maxSignals, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id], maxSignals); - expect(signalsOpen.hits.hits.length).equal(maxSignals); - }); - - it('should have recorded the rule_id within the signal', async () => { - const rule: QueryRuleCreateProps = { - ...getRuleForSignalTesting(['auditbeat-*']), - query: `_id:${ID}`, - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits[0]._source![ALERT_RULE_RULE_ID]).eql(getSimpleRule().rule_id); - }); - - it('should query and get back expected signal structure using a basic KQL query', async () => { - const rule: QueryRuleCreateProps = { - ...getRuleForSignalTesting(['auditbeat-*']), - query: `_id:${ID}`, - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - const signal = signalsOpen.hits.hits[0]._source!; - - expect(signal).eql({ - ...signal, - [ALERT_ANCESTORS]: [ - { - id: 'BhbXBmkBR346wHgn4PeZ', - type: 'event', - index: 'auditbeat-8.0.0-2019.02.19-000001', - depth: 0, - }, - ], - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DEPTH]: 1, - [ALERT_ORIGINAL_TIME]: '2019-02-19T17:40:03.790Z', - ...flattenWithPrefix(ALERT_ORIGINAL_EVENT, { - action: 'socket_closed', - dataset: 'socket', - kind: 'event', - module: 'system', - }), - }); - }); - - it('should query and get back expected signal structure using a saved query rule', async () => { - const rule: SavedQueryRuleCreateProps = { - ...getRuleForSignalTesting(['auditbeat-*']), - type: 'saved_query', - query: `_id:${ID}`, - saved_id: 'doesnt-exist', - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - const signal = signalsOpen.hits.hits[0]._source!; - expect(signal).eql({ - ...signal, - [ALERT_ANCESTORS]: [ - { - id: 'BhbXBmkBR346wHgn4PeZ', - type: 'event', - index: 'auditbeat-8.0.0-2019.02.19-000001', - depth: 0, - }, - ], - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DEPTH]: 1, - [ALERT_ORIGINAL_TIME]: '2019-02-19T17:40:03.790Z', - ...flattenWithPrefix(ALERT_ORIGINAL_EVENT, { - action: 'socket_closed', - dataset: 'socket', - kind: 'event', - module: 'system', - }), - }); - }); - - it('should query and get back expected signal structure when it is a signal on a signal', async () => { - const rule: QueryRuleCreateProps = { - ...getRuleForSignalTesting(['auditbeat-*']), - query: `_id:${ID}`, - }; - const { id: createdId } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, createdId); - await waitForSignalsToBePresent(supertest, log, 1, [createdId]); - - // Run signals on top of that 1 signal which should create a single signal (on top of) a signal - const ruleForSignals: QueryRuleCreateProps = { - ...getRuleForSignalTesting([`.alerts-security.alerts-default*`]), - rule_id: 'signal-on-signal', - }; - - const { id } = await createRule(supertest, log, ruleForSignals); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - - // Get our single signal on top of a signal - const signalsOpen = await getSignalsByRuleIds(supertest, log, ['signal-on-signal']); - - const signal = signalsOpen.hits.hits[0]._source!; - expect(signal).eql({ - ...signal, - [ALERT_ANCESTORS]: [ - { - id: 'BhbXBmkBR346wHgn4PeZ', - type: 'event', - index: 'auditbeat-8.0.0-2019.02.19-000001', - depth: 0, - }, - { - ...(signal[ALERT_ANCESTORS] as Ancestor[])[1], - type: 'signal', - index: '.internal.alerts-security.alerts-default-000001', - depth: 1, - }, - ], - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DEPTH]: 2, - [ALERT_ORIGINAL_TIME]: signal[ALERT_ORIGINAL_TIME], // original_time will always be changing sine it's based on a signal created here, so skip testing it - ...flattenWithPrefix(ALERT_ORIGINAL_EVENT, { - action: 'socket_closed', - dataset: 'socket', - kind: 'signal', - module: 'system', - }), - }); - }); - - describe('EQL Rules', () => { - before(async () => { - await esArchiver.load( - 'x-pack/test/functional/es_archives/security_solution/timestamp_override_6' - ); - }); - - after(async () => { - await esArchiver.unload( - 'x-pack/test/functional/es_archives/security_solution/timestamp_override_6' - ); - }); - - it('generates a correctly formatted signal from EQL non-sequence queries', async () => { - const rule: EqlRuleCreateProps = { - ...getEqlRuleForSignalTesting(['auditbeat-*']), - query: 'configuration where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"', - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signals = await getSignalsByIds(supertest, log, [id]); - expect(signals.hits.hits.length).eql(1); - const fullSignal = signals.hits.hits[0]._source; - if (!fullSignal) { - return expect(fullSignal).to.be.ok(); - } - - expect(fullSignal).eql({ - ...fullSignal, - agent: { - ephemeral_id: '0010d67a-14f7-41da-be30-489fea735967', - hostname: 'suricata-zeek-sensor-toronto', - id: 'a1d7b39c-f898-4dbe-a761-efb61939302d', - type: 'auditbeat', - version: '8.0.0', - }, - auditd: { - data: { - audit_enabled: '1', - old: '1', - }, - message_type: 'config_change', - result: 'success', - sequence: 1496, - session: 'unset', - summary: { - actor: { - primary: 'unset', - }, - object: { - primary: '1', - type: 'audit-config', - }, - }, - }, - cloud: { - instance: { - id: '133555295', - }, - provider: 'digitalocean', - region: 'tor1', - }, - ecs: { - version: '1.0.0-beta2', - }, - ...flattenWithPrefix('event', { - action: 'changed-audit-configuration', - category: 'configuration', - module: 'auditd', - kind: 'signal', - }), - host: { - architecture: 'x86_64', - containerized: false, - hostname: 'suricata-zeek-sensor-toronto', - id: '8cc95778cce5407c809480e8e32ad76b', - name: 'suricata-zeek-sensor-toronto', - os: { - codename: 'bionic', - family: 'debian', - kernel: '4.15.0-45-generic', - name: 'Ubuntu', - platform: 'ubuntu', - version: '18.04.2 LTS (Bionic Beaver)', - }, - }, - service: { - type: 'auditd', - }, - user: { - audit: { - id: 'unset', - }, - }, - [ALERT_REASON]: - 'configuration event on suricata-zeek-sensor-toronto created high alert Signal Testing Query.', - [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], - [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DEPTH]: 1, - [ALERT_ANCESTORS]: [ - { - depth: 0, - id: '9xbRBmkBR346wHgngz2D', - index: 'auditbeat-8.0.0-2019.02.19-000001', - type: 'event', - }, - ], - ...flattenWithPrefix(ALERT_ORIGINAL_EVENT, { - action: 'changed-audit-configuration', - category: 'configuration', - module: 'auditd', - }), - }); - }); - - it('generates up to max_signals for non-sequence EQL queries', async () => { - const rule: EqlRuleCreateProps = getEqlRuleForSignalTesting(['auditbeat-*']); - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 100, [id]); - const signals = await getSignalsByIds(supertest, log, [id], 1000); - const filteredSignals = signals.hits.hits.filter( - (signal) => signal._source?.[ALERT_DEPTH] === 1 - ); - expect(filteredSignals.length).eql(100); - }); - - it('uses the provided event_category_override', async () => { - const rule: EqlRuleCreateProps = { - ...getEqlRuleForSignalTesting(['auditbeat-*']), - query: 'config_change where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"', - event_category_override: 'auditd.message_type', - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signals = await getSignalsByIds(supertest, log, [id]); - expect(signals.hits.hits.length).eql(1); - const fullSignal = signals.hits.hits[0]._source; - if (!fullSignal) { - return expect(fullSignal).to.be.ok(); - } - - expect(fullSignal).eql({ - ...fullSignal, - auditd: { - data: { - audit_enabled: '1', - old: '1', - }, - message_type: 'config_change', - result: 'success', - sequence: 1496, - session: 'unset', - summary: { - actor: { - primary: 'unset', - }, - object: { - primary: '1', - type: 'audit-config', - }, - }, - }, - ...flattenWithPrefix('event', { - action: 'changed-audit-configuration', - category: 'configuration', - module: 'auditd', - kind: 'signal', - }), - service: { - type: 'auditd', - }, - user: { - audit: { - id: 'unset', - }, - }, - [ALERT_REASON]: - 'configuration event on suricata-zeek-sensor-toronto created high alert Signal Testing Query.', - [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], - [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DEPTH]: 1, - [ALERT_ANCESTORS]: [ - { - depth: 0, - id: '9xbRBmkBR346wHgngz2D', - index: 'auditbeat-8.0.0-2019.02.19-000001', - type: 'event', - }, - ], - ...flattenWithPrefix(ALERT_ORIGINAL_EVENT, { - action: 'changed-audit-configuration', - category: 'configuration', - module: 'auditd', - }), - }); - }); - - it('uses the provided timestamp_field', async () => { - const rule: EqlRuleCreateProps = { - ...getEqlRuleForSignalTesting(['fake.index.1']), - query: 'any where true', - timestamp_field: 'created_at', - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signals = await getSignalsByIds(supertest, log, [id]); - expect(signals.hits.hits.length).eql(3); - - const createdAtHits = signals.hits.hits.map((hit) => hit._source?.created_at); - expect(createdAtHits).to.eql([1622676785, 1622676790, 1622676795]); - }); - - it('uses the provided tiebreaker_field', async () => { - const rule: EqlRuleCreateProps = { - ...getEqlRuleForSignalTesting(['fake.index.1']), - query: 'any where true', - tiebreaker_field: 'locale', - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signals = await getSignalsByIds(supertest, log, [id]); - expect(signals.hits.hits.length).eql(3); - - const createdAtHits = signals.hits.hits.map((hit) => hit._source?.locale); - expect(createdAtHits).to.eql(['es', 'pt', 'ua']); - }); - - it('generates building block signals from EQL sequences in the expected form', async () => { - const rule: EqlRuleCreateProps = { - ...getEqlRuleForSignalTesting(['auditbeat-*']), - query: 'sequence by host.name [anomoly where true] [any where true]', // TODO: spelling - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 3, [id]); - const signals = await getSignalsByIds(supertest, log, [id]); - const buildingBlock = signals.hits.hits.find( - (signal) => - signal._source?.[ALERT_DEPTH] === 1 && - get(signal._source, ALERT_ORIGINAL_EVENT_CATEGORY) === 'anomoly' - ); - expect(buildingBlock).not.eql(undefined); - const fullSignal = buildingBlock?._source; - if (!fullSignal) { - return expect(fullSignal).to.be.ok(); - } - - expect(fullSignal).eql({ - ...fullSignal, - agent: { - ephemeral_id: '1b4978a0-48be-49b1-ac96-323425b389ab', - hostname: 'zeek-sensor-amsterdam', - id: 'e52588e6-7aa3-4c89-a2c4-d6bc5c286db1', - type: 'auditbeat', - version: '8.0.0', - }, - auditd: { - data: { - a0: '3', - a1: '107', - a2: '1', - a3: '7ffc186b58e0', - arch: 'x86_64', - auid: 'unset', - dev: 'eth0', - exit: '0', - gid: '0', - old_prom: '0', - prom: '256', - ses: 'unset', - syscall: 'setsockopt', - tty: '(none)', - uid: '0', - }, - message_type: 'anom_promiscuous', - result: 'success', - sequence: 1392, - session: 'unset', - summary: { - actor: { - primary: 'unset', - secondary: 'root', - }, - how: '/usr/bin/bro', - object: { - primary: 'eth0', - type: 'network-device', - }, - }, - }, - cloud: { instance: { id: '133551048' }, provider: 'digitalocean', region: 'ams3' }, - ecs: { version: '1.0.0-beta2' }, - ...flattenWithPrefix('event', { - action: 'changed-promiscuous-mode-on-device', - category: 'anomoly', - module: 'auditd', - kind: 'signal', - }), - host: { - architecture: 'x86_64', - containerized: false, - hostname: 'zeek-sensor-amsterdam', - id: '2ce8b1e7d69e4a1d9c6bcddc473da9d9', - name: 'zeek-sensor-amsterdam', - os: { - codename: 'bionic', - family: 'debian', - kernel: '4.15.0-45-generic', - name: 'Ubuntu', - platform: 'ubuntu', - version: '18.04.2 LTS (Bionic Beaver)', - }, - }, - process: { - executable: '/usr/bin/bro', - name: 'bro', - pid: 30157, - ppid: 30151, - title: - '/usr/bin/bro -i eth0 -U .status -p broctl -p broctl-live -p standalone -p local -p bro local.bro broctl broctl/standalone broctl', - }, - service: { type: 'auditd' }, - user: { - audit: { id: 'unset' }, - effective: { - group: { - id: '0', - name: 'root', - }, - id: '0', - name: 'root', - }, - filesystem: { - group: { - id: '0', - name: 'root', - }, - id: '0', - name: 'root', - }, - group: { id: '0', name: 'root' }, - id: '0', - name: 'root', - saved: { - group: { - id: '0', - name: 'root', - }, - id: '0', - name: 'root', - }, - }, - [ALERT_REASON]: - 'anomoly event with process bro, by root on zeek-sensor-amsterdam created high alert Signal Testing Query.', - [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], - [ALERT_GROUP_ID]: fullSignal[ALERT_GROUP_ID], - [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DEPTH]: 1, - [ALERT_ANCESTORS]: [ - { - depth: 0, - id: 'VhXOBmkBR346wHgnLP8T', - index: 'auditbeat-8.0.0-2019.02.19-000001', - type: 'event', - }, - ], - ...flattenWithPrefix(ALERT_ORIGINAL_EVENT, { - action: 'changed-promiscuous-mode-on-device', - category: 'anomoly', - module: 'auditd', - }), - }); - }); - - it('generates shell signals from EQL sequences in the expected form', async () => { - const rule: EqlRuleCreateProps = { - ...getEqlRuleForSignalTesting(['auditbeat-*']), - query: 'sequence by host.name [anomoly where true] [any where true]', - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 3, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - const sequenceSignal = signalsOpen.hits.hits.find( - (signal) => signal._source?.[ALERT_DEPTH] === 2 - ); - const source = sequenceSignal?._source; - if (!source) { - return expect(source).to.be.ok(); - } - const eventIds = (source?.[ALERT_ANCESTORS] as Ancestor[]) - .filter((event) => event.depth === 1) - .map((event) => event.id); - expect(source).eql({ - ...source, - agent: { - ephemeral_id: '1b4978a0-48be-49b1-ac96-323425b389ab', - hostname: 'zeek-sensor-amsterdam', - id: 'e52588e6-7aa3-4c89-a2c4-d6bc5c286db1', - type: 'auditbeat', - version: '8.0.0', - }, - auditd: { session: 'unset', summary: { actor: { primary: 'unset' } } }, - cloud: { instance: { id: '133551048' }, provider: 'digitalocean', region: 'ams3' }, - ecs: { version: '1.0.0-beta2' }, - [EVENT_KIND]: 'signal', - host: { - architecture: 'x86_64', - containerized: false, - hostname: 'zeek-sensor-amsterdam', - id: '2ce8b1e7d69e4a1d9c6bcddc473da9d9', - name: 'zeek-sensor-amsterdam', - os: { - codename: 'bionic', - family: 'debian', - kernel: '4.15.0-45-generic', - name: 'Ubuntu', - platform: 'ubuntu', - version: '18.04.2 LTS (Bionic Beaver)', - }, - }, - service: { type: 'auditd' }, - user: { audit: { id: 'unset' }, id: '0', name: 'root' }, - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_DEPTH]: 2, - [ALERT_GROUP_ID]: source[ALERT_GROUP_ID], - [ALERT_REASON]: - 'event by root on zeek-sensor-amsterdam created high alert Signal Testing Query.', - [ALERT_RULE_UUID]: source[ALERT_RULE_UUID], - [ALERT_ANCESTORS]: [ - { - depth: 0, - id: 'VhXOBmkBR346wHgnLP8T', - index: 'auditbeat-8.0.0-2019.02.19-000001', - type: 'event', - }, - { - depth: 1, - id: eventIds[0], - index: '', - rule: source[ALERT_RULE_UUID], - type: 'signal', - }, - { - depth: 0, - id: '4hbXBmkBR346wHgn6fdp', - index: 'auditbeat-8.0.0-2019.02.19-000001', - type: 'event', - }, - { - depth: 1, - id: eventIds[1], - index: '', - rule: source[ALERT_RULE_UUID], - type: 'signal', - }, - ], - }); - }); - - it('generates up to max_signals with an EQL rule', async () => { - const rule: EqlRuleCreateProps = { - ...getEqlRuleForSignalTesting(['auditbeat-*']), - query: 'sequence by host.name [any where true] [any where true]', - max_signals: 200, - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - // For EQL rules, max_signals is the maximum number of detected sequences: each sequence has a building block - // alert for each event in the sequence, so max_signals=200 results in 400 building blocks in addition to - // 200 regular alerts - await waitForSignalsToBePresent(supertest, log, 600, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id], 1000); - expect(signalsOpen.hits.hits.length).eql(600); - const shellSignals = signalsOpen.hits.hits.filter( - (signal) => signal._source?.[ALERT_DEPTH] === 2 - ); - const buildingBlocks = signalsOpen.hits.hits.filter( - (signal) => signal._source?.[ALERT_DEPTH] === 1 - ); - expect(shellSignals.length).eql(200); - expect(buildingBlocks.length).eql(400); - }); - - it('generates signals when an index name contains special characters to encode', async () => { - const rule: EqlRuleCreateProps = { - ...getEqlRuleForSignalTesting(['auditbeat-*', '']), - query: 'configuration where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"', - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signals = await getSignalsByIds(supertest, log, [id]); - expect(signals.hits.hits.length).eql(1); - }); - - it('uses the provided filters', async () => { - const rule: EqlRuleCreateProps = { - ...getEqlRuleForSignalTesting(['auditbeat-*']), - query: 'any where true', - filters: [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'source.ip', - params: { - query: '46.148.18.163', - }, - }, - query: { - match_phrase: { - 'source.ip': '46.148.18.163', - }, - }, - }, - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'event.action', - params: { - query: 'error', - }, - }, - query: { - match_phrase: { - 'event.action': 'error', - }, - }, - }, - ], - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signals = await getSignalsByIds(supertest, log, [id]); - expect(signals.hits.hits.length).eql(2); - }); - - describe('EQL alerts should be be enriched', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk'); - }); - - it('should be enriched with host risk score', async () => { - const rule: EqlRuleCreateProps = { - ...getEqlRuleForSignalTesting(['auditbeat-*']), - query: 'configuration where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"', - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signals = await getSignalsByIds(supertest, log, [id]); - expect(signals.hits.hits.length).eql(1); - const fullSignal = signals.hits.hits[0]._source; - if (!fullSignal) { - return expect(fullSignal).to.be.ok(); - } - expect(fullSignal?.host?.risk?.calculated_level).to.eql('Critical'); - expect(fullSignal?.host?.risk?.calculated_score_norm).to.eql(96); - }); - }); - }); - - describe('Threshold Rules', () => { - it('generates 1 signal from Threshold rules when threshold is met', async () => { - const rule: ThresholdRuleCreateProps = { - ...getThresholdRuleForSignalTesting(['auditbeat-*']), - threshold: { - field: ['host.id'], - value: 700, - }, - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits.length).eql(1); - const fullSignal = signalsOpen.hits.hits[0]._source; - if (!fullSignal) { - return expect(fullSignal).to.be.ok(); - } - const eventIds = (fullSignal?.[ALERT_ANCESTORS] as Ancestor[]).map((event) => event.id); - expect(fullSignal).eql({ - ...fullSignal, - 'host.id': '8cc95778cce5407c809480e8e32ad76b', - [EVENT_KIND]: 'signal', - [ALERT_ANCESTORS]: [ - { - depth: 0, - id: eventIds[0], - index: 'auditbeat-*', - type: 'event', - }, - ], - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_REASON]: 'event created high alert Signal Testing Query.', - [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], - [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], - [ALERT_DEPTH]: 1, - [ALERT_THRESHOLD_RESULT]: { - terms: [ - { - field: 'host.id', - value: '8cc95778cce5407c809480e8e32ad76b', - }, - ], - count: 788, - from: '2019-02-19T07:12:05.332Z', - }, - }); - }); - - it('generates 2 signals from Threshold rules when threshold is met', async () => { - const rule: ThresholdRuleCreateProps = { - ...getThresholdRuleForSignalTesting(['auditbeat-*']), - threshold: { - field: 'host.id', - value: 100, - }, - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 2, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits.length).eql(2); - }); - - it('applies the provided query before bucketing ', async () => { - const rule: ThresholdRuleCreateProps = { - ...getThresholdRuleForSignalTesting(['auditbeat-*']), - query: 'host.id:"2ab45fc1c41e4c84bbd02202a7e5761f"', - threshold: { - field: 'process.name', - value: 21, - }, - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits.length).eql(1); - }); - - it('generates no signals from Threshold rules when threshold is met and cardinality is not met', async () => { - const rule: ThresholdRuleCreateProps = { - ...getThresholdRuleForSignalTesting(['auditbeat-*']), - threshold: { - field: 'host.id', - value: 100, - cardinality: [ - { - field: 'destination.ip', - value: 100, - }, - ], - }, - }; - const createdRule = await createRule(supertest, log, rule); - const signalsOpen = await getOpenSignals(supertest, log, es, createdRule); - expect(signalsOpen.hits.hits.length).eql(0); - }); - - it('generates no signals from Threshold rules when cardinality is met and threshold is not met', async () => { - const rule: ThresholdRuleCreateProps = { - ...getThresholdRuleForSignalTesting(['auditbeat-*']), - threshold: { - field: 'host.id', - value: 1000, - cardinality: [ - { - field: 'destination.ip', - value: 5, - }, - ], - }, - }; - const createdRule = await createRule(supertest, log, rule); - const signalsOpen = await getOpenSignals(supertest, log, es, createdRule); - expect(signalsOpen.hits.hits.length).eql(0); - }); - - it('generates signals from Threshold rules when threshold and cardinality are both met', async () => { - const rule: ThresholdRuleCreateProps = { - ...getThresholdRuleForSignalTesting(['auditbeat-*']), - threshold: { - field: 'host.id', - value: 100, - cardinality: [ - { - field: 'destination.ip', - value: 5, - }, - ], - }, - }; - const createdRule = await createRule(supertest, log, rule); - const signalsOpen = await getOpenSignals(supertest, log, es, createdRule); - expect(signalsOpen.hits.hits.length).eql(1); - const fullSignal = signalsOpen.hits.hits[0]._source; - if (!fullSignal) { - return expect(fullSignal).to.be.ok(); - } - const eventIds = (fullSignal?.[ALERT_ANCESTORS] as Ancestor[]).map((event) => event.id); - expect(fullSignal).eql({ - ...fullSignal, - 'host.id': '8cc95778cce5407c809480e8e32ad76b', - [EVENT_KIND]: 'signal', - [ALERT_ANCESTORS]: [ - { - depth: 0, - id: eventIds[0], - index: 'auditbeat-*', - type: 'event', - }, - ], - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_REASON]: `event created high alert Signal Testing Query.`, - [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], - [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], - [ALERT_DEPTH]: 1, - [ALERT_THRESHOLD_RESULT]: { - terms: [ - { - field: 'host.id', - value: '8cc95778cce5407c809480e8e32ad76b', - }, - ], - cardinality: [ - { - field: 'destination.ip', - value: 7, - }, - ], - count: 788, - from: '2019-02-19T07:12:05.332Z', - }, - }); - }); - - it('should not generate signals if only one field meets the threshold requirement', async () => { - const rule: ThresholdRuleCreateProps = { - ...getThresholdRuleForSignalTesting(['auditbeat-*']), - threshold: { - field: ['host.id', 'process.name'], - value: 22, - }, - }; - const createdRule = await createRule(supertest, log, rule); - const signalsOpen = await getOpenSignals(supertest, log, es, createdRule); - expect(signalsOpen.hits.hits.length).eql(0); - }); - - it('generates signals from Threshold rules when bucketing by multiple fields', async () => { - const rule: ThresholdRuleCreateProps = { - ...getThresholdRuleForSignalTesting(['auditbeat-*']), - threshold: { - field: ['host.id', 'process.name', 'event.module'], - value: 21, - }, - }; - const createdRule = await createRule(supertest, log, rule); - const signalsOpen = await getOpenSignals(supertest, log, es, createdRule); - expect(signalsOpen.hits.hits.length).eql(1); - const fullSignal = signalsOpen.hits.hits[0]._source; - if (!fullSignal) { - return expect(fullSignal).to.be.ok(); - } - const eventIds = (fullSignal[ALERT_ANCESTORS] as Ancestor[]).map((event) => event.id); - expect(fullSignal).eql({ - ...fullSignal, - 'event.module': 'system', - 'host.id': '2ab45fc1c41e4c84bbd02202a7e5761f', - 'process.name': 'sshd', - [EVENT_KIND]: 'signal', - [ALERT_ANCESTORS]: [ - { - depth: 0, - id: eventIds[0], - index: 'auditbeat-*', - type: 'event', - }, - ], - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_REASON]: `event with process sshd, created high alert Signal Testing Query.`, - [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], - [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], - [ALERT_DEPTH]: 1, - [ALERT_THRESHOLD_RESULT]: { - terms: [ - { - field: 'host.id', - value: '2ab45fc1c41e4c84bbd02202a7e5761f', - }, - { - field: 'process.name', - value: 'sshd', - }, - { - field: 'event.module', - value: 'system', - }, - ], - count: 21, - from: '2019-02-19T20:22:03.561Z', - }, - }); - }); - - describe('Timestamp override and fallback', async () => { - before(async () => { - await esArchiver.load( - 'x-pack/test/functional/es_archives/security_solution/timestamp_fallback' - ); - }); - - after(async () => { - await esArchiver.unload( - 'x-pack/test/functional/es_archives/security_solution/timestamp_fallback' - ); - }); - - it('applies timestamp override when using single field', async () => { - const rule: ThresholdRuleCreateProps = { - ...getThresholdRuleForSignalTesting(['timestamp-fallback-test']), - threshold: { - field: 'host.name', - value: 1, - }, - timestamp_override: 'event.ingested', - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 2, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits.length).eql(4); - - for (const hit of signalsOpen.hits.hits) { - const originalTime = hit._source?.[ALERT_ORIGINAL_TIME]; - const hostName = hit._source?.['host.name']; - if (hostName === 'host-1') { - expect(originalTime).eql('2020-12-16T15:15:18.570Z'); - } else if (hostName === 'host-2') { - expect(originalTime).eql('2020-12-16T15:16:18.570Z'); - } else if (hostName === 'host-3') { - expect(originalTime).eql('2020-12-16T16:15:18.570Z'); - } else { - expect(originalTime).eql('2020-12-16T16:16:18.570Z'); - } - } - }); - - it('applies timestamp override when using multiple fields', async () => { - const rule: ThresholdRuleCreateProps = { - ...getThresholdRuleForSignalTesting(['timestamp-fallback-test']), - threshold: { - field: ['host.name', 'source.ip'], - value: 1, - }, - timestamp_override: 'event.ingested', - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 2, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits.length).eql(4); - - for (const hit of signalsOpen.hits.hits) { - const originalTime = hit._source?.[ALERT_ORIGINAL_TIME]; - const hostName = hit._source?.['host.name']; - if (hostName === 'host-1') { - expect(originalTime).eql('2020-12-16T15:15:18.570Z'); - } else if (hostName === 'host-2') { - expect(originalTime).eql('2020-12-16T15:16:18.570Z'); - } else if (hostName === 'host-3') { - expect(originalTime).eql('2020-12-16T16:15:18.570Z'); - } else { - expect(originalTime).eql('2020-12-16T16:16:18.570Z'); - } - } - }); - }); - - describe('Threshold alerts should be be enriched', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk'); - }); - - it('should be enriched with host risk score', async () => { - const rule: ThresholdRuleCreateProps = { - ...getThresholdRuleForSignalTesting(['auditbeat-*']), - threshold: { - field: 'host.name', - value: 100, - }, - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 2, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - - expect(signalsOpen.hits.hits[0]?._source?.host?.risk?.calculated_level).to.eql('Low'); - expect(signalsOpen.hits.hits[0]?._source?.host?.risk?.calculated_score_norm).to.eql(20); - expect(signalsOpen.hits.hits[1]?._source?.host?.risk?.calculated_level).to.eql( - 'Critical' - ); - expect(signalsOpen.hits.hits[1]?._source?.host?.risk?.calculated_score_norm).to.eql(96); - }); - }); - }); - - describe('Enrich alerts: query rule', () => { - describe('without index avalable', () => { - it('should do not have risk score fields', async () => { - const rule: QueryRuleCreateProps = { - ...getRuleForSignalTesting(['auditbeat-*']), - query: `_id:${ID}`, - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits[0]?._source?.host?.risk).to.eql(undefined); - expect(signalsOpen.hits.hits[0]?._source?.user?.risk).to.eql(undefined); - }); - }); - - describe('with host risk score', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); - await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); - await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk'); - }); - - it('should host have risk score field and do not have user risk score', async () => { - const rule: QueryRuleCreateProps = { - ...getRuleForSignalTesting(['auditbeat-*']), - query: `_id:${ID} or _id:GBbXBmkBR346wHgn5_eR or _id:x10zJ2oE9v5HJNSHhyxi`, - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - - const alerts = signalsOpen.hits.hits ?? []; - const firstAlert = alerts.find( - (alert) => alert?._source?.host?.name === 'suricata-zeek-sensor-toronto' - ); - const secondAlert = alerts.find( - (alert) => alert?._source?.host?.name === 'suricata-sensor-london' - ); - const thirdAlert = alerts.find((alert) => alert?._source?.host?.name === 'IE11WIN8_1'); - - expect(firstAlert?._source?.host?.risk?.calculated_level).to.eql('Critical'); - expect(firstAlert?._source?.host?.risk?.calculated_score_norm).to.eql(96); - expect(firstAlert?._source?.user?.risk).to.eql(undefined); - expect(secondAlert?._source?.host?.risk?.calculated_level).to.eql('Low'); - expect(secondAlert?._source?.host?.risk?.calculated_score_norm).to.eql(20); - expect(thirdAlert?._source?.host?.risk).to.eql(undefined); - }); - }); - - describe('with host and risk score and user risk score', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); - await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk'); - await esArchiver.load('x-pack/test/functional/es_archives/entity/user_risk'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); - await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk'); - await esArchiver.unload('x-pack/test/functional/es_archives/entity/user_risk'); - }); - - it('should have host and user risk score fields', async () => { - const rule: QueryRuleCreateProps = { - ...getRuleForSignalTesting(['auditbeat-*']), - query: `_id:${ID}`, - }; - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signalsOpen = await getSignalsByIds(supertest, log, [id]); - expect(signalsOpen.hits.hits[0]?._source?.host?.risk?.calculated_level).to.eql( - 'Critical' - ); - expect(signalsOpen.hits.hits[0]?._source?.host?.risk?.calculated_score_norm).to.eql(96); - expect(signalsOpen.hits.hits[0]?._source?.user?.risk?.calculated_level).to.eql('Low'); - expect(signalsOpen.hits.hits[0]?._source?.user?.risk?.calculated_score_norm).to.eql(11); - }); - }); - }); - }); - - /** - * Here we test the functionality of Severity and Risk Score overrides (also called "mappings" - * in the code). If the rule specifies a mapping, then the final Severity or Risk Score - * value of the signal will be taken from the mapped field of the source event. - */ - describe('Signals generated from events with custom severity and risk score fields', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/signals/severity_risk_overrides'); - }); - - after(async () => { - await esArchiver.unload( - 'x-pack/test/functional/es_archives/signals/severity_risk_overrides' - ); - }); - - const executeRuleAndGetSignals = async (rule: QueryRuleCreateProps) => { - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 4, [id]); - const signalsResponse = await getSignalsByIds(supertest, log, [id]); - const signals = signalsResponse.hits.hits.map((hit) => hit._source); - const signalsOrderedByEventId = orderBy(signals, 'signal.parent.id', 'asc'); - return signalsOrderedByEventId; - }; - - it('should get default severity and risk score if there is no mapping', async () => { - const rule: QueryRuleCreateProps = { - ...getRuleForSignalTesting(['signal_overrides']), - severity: 'medium', - risk_score: 75, - }; - - const signals = await executeRuleAndGetSignals(rule); - - expect(signals.length).equal(4); - signals.forEach((s) => { - expect(s?.[ALERT_SEVERITY]).equal('medium'); - expect(s?.[ALERT_RULE_PARAMETERS].severity_mapping).eql([]); - - expect(s?.[ALERT_RISK_SCORE]).equal(75); - expect(s?.[ALERT_RULE_PARAMETERS].risk_score_mapping).eql([]); - }); - }); - - it('should get overridden severity if the rule has a mapping for it', async () => { - const rule: QueryRuleCreateProps = { - ...getRuleForSignalTesting(['signal_overrides']), - severity: 'medium', - severity_mapping: [ - { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, - { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, - ], - risk_score: 75, - }; - - const signals = await executeRuleAndGetSignals(rule); - const severities = signals.map((s) => ({ - id: (s?.[ALERT_ANCESTORS] as Ancestor[])[0].id, - value: s?.[ALERT_SEVERITY], - })); - - expect(signals.length).equal(4); - expect(severities).eql([ - { id: '1', value: 'high' }, - { id: '2', value: 'critical' }, - { id: '3', value: 'critical' }, - { id: '4', value: 'critical' }, - ]); - - signals.forEach((s) => { - expect(s?.[ALERT_RISK_SCORE]).equal(75); - expect(s?.[ALERT_RULE_PARAMETERS].risk_score_mapping).eql([]); - expect(s?.[ALERT_RULE_PARAMETERS].severity_mapping).eql([ - { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, - { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, - ]); - }); - }); - - it('should get overridden risk score if the rule has a mapping for it', async () => { - const rule: QueryRuleCreateProps = { - ...getRuleForSignalTesting(['signal_overrides']), - severity: 'medium', - risk_score: 75, - risk_score_mapping: [ - { field: 'my_risk', operator: 'equals', value: '', risk_score: undefined }, - ], - }; - - const signals = await executeRuleAndGetSignals(rule); - const riskScores = signals.map((s) => ({ - id: (s?.[ALERT_ANCESTORS] as Ancestor[])[0].id, - value: s?.[ALERT_RISK_SCORE], - })); - - expect(signals.length).equal(4); - expect(riskScores).eql([ - { id: '1', value: 31.14 }, - { id: '2', value: 32.14 }, - { id: '3', value: 33.14 }, - { id: '4', value: 34.14 }, - ]); - - signals.forEach((s) => { - expect(s?.[ALERT_SEVERITY]).equal('medium'); - expect(s?.[ALERT_RULE_PARAMETERS].severity_mapping).eql([]); - expect(s?.[ALERT_RULE_PARAMETERS].risk_score_mapping).eql([ - { field: 'my_risk', operator: 'equals', value: '' }, - ]); - }); - }); - - it('should get overridden severity and risk score if the rule has both mappings', async () => { - const rule: QueryRuleCreateProps = { - ...getRuleForSignalTesting(['signal_overrides']), - severity: 'medium', - severity_mapping: [ - { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, - { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, - ], - risk_score: 75, - risk_score_mapping: [ - { field: 'my_risk', operator: 'equals', value: '', risk_score: undefined }, - ], - }; - - const signals = await executeRuleAndGetSignals(rule); - const values = signals.map((s) => ({ - id: (s?.[ALERT_ANCESTORS] as Ancestor[])[0].id, - severity: s?.[ALERT_SEVERITY], - risk: s?.[ALERT_RISK_SCORE], - })); - - expect(signals.length).equal(4); - expect(values).eql([ - { id: '1', severity: 'high', risk: 31.14 }, - { id: '2', severity: 'critical', risk: 32.14 }, - { id: '3', severity: 'critical', risk: 33.14 }, - { id: '4', severity: 'critical', risk: 34.14 }, - ]); - - signals.forEach((s) => { - expect(s?.[ALERT_RULE_PARAMETERS].severity_mapping).eql([ - { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, - { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, - ]); - expect(s?.[ALERT_RULE_PARAMETERS].risk_score_mapping).eql([ - { field: 'my_risk', operator: 'equals', value: '' }, - ]); - }); - }); - }); - - describe('Signals generated from events with name override field', async () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - beforeEach(async () => { - await deleteSignalsIndex(supertest, log); - await createSignalsIndex(supertest, log); - }); - - afterEach(async () => { - await deleteSignalsIndex(supertest, log); - await deleteAllAlerts(supertest, log); - }); - - it('should generate signals with name_override field', async () => { - const rule: QueryRuleCreateProps = { - ...getRuleForSignalTesting(['auditbeat-*']), - rule_name_override: 'event.action', - }; - - const { id } = await createRule(supertest, log, rule); - - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signalsResponse = await getSignalsByIds(supertest, log, [id], 1); - const signals = signalsResponse.hits.hits.map((hit) => hit._source); - const signalsOrderedByEventId = orderBy(signals, 'signal.parent.id', 'asc'); - const fullSignal = signalsOrderedByEventId[0]; - if (!fullSignal) { - return expect(fullSignal).to.be.ok(); - } - - expect(fullSignal).eql({ - ...fullSignal, - [EVENT_ACTION]: 'boot', - [ALERT_ANCESTORS]: [ - { - depth: 0, - id: 'UBXOBmkBR346wHgnLP8T', - index: 'auditbeat-8.0.0-2019.02.19-000001', - type: 'event', - }, - ], - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_REASON]: `event on zeek-sensor-amsterdam created high alert boot.`, - [ALERT_RULE_NAME]: 'boot', - [ALERT_RULE_RULE_NAME_OVERRIDE]: 'event.action', - [ALERT_DEPTH]: 1, - ...flattenWithPrefix(ALERT_ORIGINAL_EVENT, { - action: 'boot', - dataset: 'login', - kind: 'event', - module: 'system', - origin: '/var/log/wtmp', - }), - }); - }); - }); - - describe('Signal deduplication', async () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - beforeEach(async () => { - await deleteSignalsIndex(supertest, log); - }); - - afterEach(async () => { - await deleteSignalsIndex(supertest, log); - await deleteAllAlerts(supertest, log); - }); - - it('should not generate duplicate signals', async () => { - const rule: QueryRuleCreateProps = { - ...getRuleForSignalTesting(['auditbeat-*']), - query: `_id:${ID}`, - }; - - const ruleResponse = await createRule(supertest, log, rule); - - const signals = await getOpenSignals(supertest, log, es, ruleResponse); - expect(signals.hits.hits.length).to.eql(1); - - const statusResponse = await supertest - .get(DETECTION_ENGINE_RULES_URL) - .set('kbn-xsrf', 'true') - .query({ id: ruleResponse.id }); - - // TODO: https://github.com/elastic/kibana/pull/121644 clean up, make type-safe - const ruleStatusDate = statusResponse.body?.execution_summary?.last_execution.date; - const initialStatusDate = new Date(ruleStatusDate); - - const initialSignal = signals.hits.hits[0]; - - // Disable the rule then re-enable to trigger another run - await supertest - .patch(DETECTION_ENGINE_RULES_URL) - .set('kbn-xsrf', 'true') - .send({ rule_id: ruleResponse.rule_id, enabled: false }) - .expect(200); - - await supertest - .patch(DETECTION_ENGINE_RULES_URL) - .set('kbn-xsrf', 'true') - .send({ rule_id: ruleResponse.rule_id, enabled: true }) - .expect(200); - - await waitForRuleSuccessOrStatus( - supertest, - log, - ruleResponse.id, - RuleExecutionStatus.succeeded, - initialStatusDate - ); - - const newSignals = await getOpenSignals(supertest, log, es, ruleResponse); - expect(newSignals.hits.hits.length).to.eql(1); - expect(newSignals.hits.hits[0]).to.eql(initialSignal); - }); - }); - }); -}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts index 3064d412da1bd..690498a287530 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts @@ -23,16 +23,13 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./create_rules')); loadTestFile(require.resolve('./preview_rules')); loadTestFile(require.resolve('./create_rules_bulk')); - loadTestFile(require.resolve('./create_ml')); loadTestFile(require.resolve('./create_new_terms')); - loadTestFile(require.resolve('./create_threat_matching')); loadTestFile(require.resolve('./create_rule_exceptions')); loadTestFile(require.resolve('./delete_rules')); loadTestFile(require.resolve('./delete_rules_bulk')); loadTestFile(require.resolve('./export_rules')); loadTestFile(require.resolve('./find_rules')); loadTestFile(require.resolve('./find_rule_exception_references')); - loadTestFile(require.resolve('./generating_signals')); loadTestFile(require.resolve('./get_prepackaged_rules_status')); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/preview_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/preview_rules.ts index 3e0dd166a0cd2..b38545e9c03c9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/preview_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/preview_rules.ts @@ -20,8 +20,8 @@ export default ({ getService }: FtrProviderContext) => { const supertestWithoutAuth = getService('supertestWithoutAuth'); const log = getService('log'); - describe('create_rules', () => { - describe('creating rules', () => { + describe('preview_rules', () => { + describe('previewing rules', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/README.md b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/README.md new file mode 100644 index 0000000000000..3a72c90e3ec54 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/README.md @@ -0,0 +1,11 @@ +### Security Rule Execution Logic Tests + +These tests use the rule preview API as a fast way to verify that rules generate alerts as expected with various parameter settings. This avoids the costly overhead of creating a real rule and waiting for it to be scheduled. The preview route also returns rule statuses directly in the API response instead of writing the statuses to saved objects, which saves significant time as well. + +For assurance that the real rule execution works, one test for each rule type still creates a real rule and waits for the execution through the alerting framework and resulting alerts. + +As a result, the tests here typically run ~10x faster than the tests they replaced that were creating actual rules and running them. We can therefore add more tests here and get better coverage of the rule execution logic (which is currently, as of 8.5, somewhat lacking). + +Since the rule execution logic is primarily focused around generating and executing Elasticsearch queries, we need significant testing around whether or not the queries are returning the expected results. This is not achievable with unit tests at the moment, since we need to mock Elasticsearch results. The tests here are the preferred way to ensure that rules are executing the correct logic to generate alerts from source data. + +Testing rules with exceptions is still slow, even with the preview API, because the exception list has to be created for real and then cleaned up after the test - exceptions live in saved objects, so creating exceptions for individual tests slows them down significantly (>1s per test vs ~200ms for a test without exceptions). This is an area for future improvement. diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/config.ts new file mode 100644 index 0000000000000..2430b8f2148d9 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/config.ts @@ -0,0 +1,18 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/eql.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/eql.ts new file mode 100644 index 0000000000000..cffc0a311ef3f --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/eql.ts @@ -0,0 +1,606 @@ +/* + * 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 expect from '@kbn/expect'; +import { + ALERT_REASON, + ALERT_RULE_UUID, + ALERT_WORKFLOW_STATUS, + EVENT_KIND, +} from '@kbn/rule-data-utils'; +import { flattenWithPrefix } from '@kbn/securitysolution-rules'; + +import { get } from 'lodash'; + +import { EqlRuleCreateProps } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; +import { Ancestor } from '@kbn/security-solution-plugin/server/lib/detection_engine/signals/types'; +import { + ALERT_ANCESTORS, + ALERT_DEPTH, + ALERT_ORIGINAL_TIME, + ALERT_ORIGINAL_EVENT, + ALERT_ORIGINAL_EVENT_CATEGORY, + ALERT_GROUP_ID, +} from '@kbn/security-solution-plugin/common/field_maps/field_names'; +import { + createRule, + deleteAllAlerts, + deleteSignalsIndex, + getEqlRuleForSignalTesting, + getOpenSignals, + getPreviewAlerts, + previewRule, +} from '../../utils'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + const log = getService('log'); + + describe('EQL type rules', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); + await esArchiver.load( + 'x-pack/test/functional/es_archives/security_solution/timestamp_override_6' + ); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/timestamp_override_6' + ); + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + }); + + // First test creates a real rule - remaining tests use preview API + it('generates a correctly formatted signal from EQL non-sequence queries', async () => { + const rule: EqlRuleCreateProps = { + ...getEqlRuleForSignalTesting(['auditbeat-*']), + query: 'configuration where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"', + }; + const createdRule = await createRule(supertest, log, rule); + const alerts = await getOpenSignals(supertest, log, es, createdRule); + expect(alerts.hits.hits.length).eql(1); + const fullSignal = alerts.hits.hits[0]._source; + if (!fullSignal) { + return expect(fullSignal).to.be.ok(); + } + + expect(fullSignal).eql({ + ...fullSignal, + agent: { + ephemeral_id: '0010d67a-14f7-41da-be30-489fea735967', + hostname: 'suricata-zeek-sensor-toronto', + id: 'a1d7b39c-f898-4dbe-a761-efb61939302d', + type: 'auditbeat', + version: '8.0.0', + }, + auditd: { + data: { + audit_enabled: '1', + old: '1', + }, + message_type: 'config_change', + result: 'success', + sequence: 1496, + session: 'unset', + summary: { + actor: { + primary: 'unset', + }, + object: { + primary: '1', + type: 'audit-config', + }, + }, + }, + cloud: { + instance: { + id: '133555295', + }, + provider: 'digitalocean', + region: 'tor1', + }, + ecs: { + version: '1.0.0-beta2', + }, + ...flattenWithPrefix('event', { + action: 'changed-audit-configuration', + category: 'configuration', + module: 'auditd', + kind: 'signal', + }), + host: { + architecture: 'x86_64', + containerized: false, + hostname: 'suricata-zeek-sensor-toronto', + id: '8cc95778cce5407c809480e8e32ad76b', + name: 'suricata-zeek-sensor-toronto', + os: { + codename: 'bionic', + family: 'debian', + kernel: '4.15.0-45-generic', + name: 'Ubuntu', + platform: 'ubuntu', + version: '18.04.2 LTS (Bionic Beaver)', + }, + }, + service: { + type: 'auditd', + }, + user: { + audit: { + id: 'unset', + }, + }, + [ALERT_REASON]: + 'configuration event on suricata-zeek-sensor-toronto created high alert Signal Testing Query.', + [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], + [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DEPTH]: 1, + [ALERT_ANCESTORS]: [ + { + depth: 0, + id: '9xbRBmkBR346wHgngz2D', + index: 'auditbeat-8.0.0-2019.02.19-000001', + type: 'event', + }, + ], + ...flattenWithPrefix(ALERT_ORIGINAL_EVENT, { + action: 'changed-audit-configuration', + category: 'configuration', + module: 'auditd', + }), + }); + }); + + it('generates up to max_signals for non-sequence EQL queries', async () => { + const maxSignals = 200; + const rule: EqlRuleCreateProps = { + ...getEqlRuleForSignalTesting(['auditbeat-*']), + max_signals: maxSignals, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId, size: maxSignals * 2 }); + expect(previewAlerts.length).eql(maxSignals); + }); + + it('uses the provided event_category_override', async () => { + const rule: EqlRuleCreateProps = { + ...getEqlRuleForSignalTesting(['auditbeat-*']), + query: 'config_change where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"', + event_category_override: 'auditd.message_type', + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).eql(1); + const fullSignal = previewAlerts[0]._source; + if (!fullSignal) { + return expect(fullSignal).to.be.ok(); + } + + expect(fullSignal).eql({ + ...fullSignal, + auditd: { + data: { + audit_enabled: '1', + old: '1', + }, + message_type: 'config_change', + result: 'success', + sequence: 1496, + session: 'unset', + summary: { + actor: { + primary: 'unset', + }, + object: { + primary: '1', + type: 'audit-config', + }, + }, + }, + ...flattenWithPrefix('event', { + action: 'changed-audit-configuration', + category: 'configuration', + module: 'auditd', + kind: 'signal', + }), + service: { + type: 'auditd', + }, + user: { + audit: { + id: 'unset', + }, + }, + [ALERT_REASON]: + 'configuration event on suricata-zeek-sensor-toronto created high alert Signal Testing Query.', + [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], + [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DEPTH]: 1, + [ALERT_ANCESTORS]: [ + { + depth: 0, + id: '9xbRBmkBR346wHgngz2D', + index: 'auditbeat-8.0.0-2019.02.19-000001', + type: 'event', + }, + ], + ...flattenWithPrefix(ALERT_ORIGINAL_EVENT, { + action: 'changed-audit-configuration', + category: 'configuration', + module: 'auditd', + }), + }); + }); + + it('uses the provided timestamp_field', async () => { + const rule: EqlRuleCreateProps = { + ...getEqlRuleForSignalTesting(['fake.index.1']), + query: 'any where true', + timestamp_field: 'created_at', + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).eql(3); + + const createdAtHits = previewAlerts.map((hit) => hit._source?.created_at).sort(); + expect(createdAtHits).to.eql([1622676785, 1622676790, 1622676795]); + }); + + it('uses the provided tiebreaker_field', async () => { + const rule: EqlRuleCreateProps = { + ...getEqlRuleForSignalTesting(['fake.index.1']), + query: 'any where true', + tiebreaker_field: 'locale', + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).eql(3); + + const createdAtHits = previewAlerts.map((hit) => hit._source?.locale); + expect(createdAtHits).to.eql(['es', 'pt', 'ua']); + }); + + it('generates building block signals from EQL sequences in the expected form', async () => { + const rule: EqlRuleCreateProps = { + ...getEqlRuleForSignalTesting(['auditbeat-*']), + query: 'sequence by host.name [anomoly where true] [any where true]', // TODO: spelling + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + const buildingBlock = previewAlerts.find( + (alert) => + alert._source?.[ALERT_DEPTH] === 1 && + get(alert._source, ALERT_ORIGINAL_EVENT_CATEGORY) === 'anomoly' + ); + expect(buildingBlock).not.eql(undefined); + const fullSignal = buildingBlock?._source; + if (!fullSignal) { + return expect(fullSignal).to.be.ok(); + } + + expect(fullSignal).eql({ + ...fullSignal, + agent: { + ephemeral_id: '1b4978a0-48be-49b1-ac96-323425b389ab', + hostname: 'zeek-sensor-amsterdam', + id: 'e52588e6-7aa3-4c89-a2c4-d6bc5c286db1', + type: 'auditbeat', + version: '8.0.0', + }, + auditd: { + data: { + a0: '3', + a1: '107', + a2: '1', + a3: '7ffc186b58e0', + arch: 'x86_64', + auid: 'unset', + dev: 'eth0', + exit: '0', + gid: '0', + old_prom: '0', + prom: '256', + ses: 'unset', + syscall: 'setsockopt', + tty: '(none)', + uid: '0', + }, + message_type: 'anom_promiscuous', + result: 'success', + sequence: 1392, + session: 'unset', + summary: { + actor: { + primary: 'unset', + secondary: 'root', + }, + how: '/usr/bin/bro', + object: { + primary: 'eth0', + type: 'network-device', + }, + }, + }, + cloud: { instance: { id: '133551048' }, provider: 'digitalocean', region: 'ams3' }, + ecs: { version: '1.0.0-beta2' }, + ...flattenWithPrefix('event', { + action: 'changed-promiscuous-mode-on-device', + category: 'anomoly', + module: 'auditd', + kind: 'signal', + }), + host: { + architecture: 'x86_64', + containerized: false, + hostname: 'zeek-sensor-amsterdam', + id: '2ce8b1e7d69e4a1d9c6bcddc473da9d9', + name: 'zeek-sensor-amsterdam', + os: { + codename: 'bionic', + family: 'debian', + kernel: '4.15.0-45-generic', + name: 'Ubuntu', + platform: 'ubuntu', + version: '18.04.2 LTS (Bionic Beaver)', + }, + }, + process: { + executable: '/usr/bin/bro', + name: 'bro', + pid: 30157, + ppid: 30151, + title: + '/usr/bin/bro -i eth0 -U .status -p broctl -p broctl-live -p standalone -p local -p bro local.bro broctl broctl/standalone broctl', + }, + service: { type: 'auditd' }, + user: { + audit: { id: 'unset' }, + effective: { + group: { + id: '0', + name: 'root', + }, + id: '0', + name: 'root', + }, + filesystem: { + group: { + id: '0', + name: 'root', + }, + id: '0', + name: 'root', + }, + group: { id: '0', name: 'root' }, + id: '0', + name: 'root', + saved: { + group: { + id: '0', + name: 'root', + }, + id: '0', + name: 'root', + }, + }, + [ALERT_REASON]: + 'anomoly event with process bro, by root on zeek-sensor-amsterdam created high alert Signal Testing Query.', + [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], + [ALERT_GROUP_ID]: fullSignal[ALERT_GROUP_ID], + [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DEPTH]: 1, + [ALERT_ANCESTORS]: [ + { + depth: 0, + id: 'VhXOBmkBR346wHgnLP8T', + index: 'auditbeat-8.0.0-2019.02.19-000001', + type: 'event', + }, + ], + ...flattenWithPrefix(ALERT_ORIGINAL_EVENT, { + action: 'changed-promiscuous-mode-on-device', + category: 'anomoly', + module: 'auditd', + }), + }); + }); + + it('generates shell signals from EQL sequences in the expected form', async () => { + const rule: EqlRuleCreateProps = { + ...getEqlRuleForSignalTesting(['auditbeat-*']), + query: 'sequence by host.name [anomoly where true] [any where true]', + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + const sequenceAlert = previewAlerts.find((alert) => alert._source?.[ALERT_DEPTH] === 2); + const source = sequenceAlert?._source; + if (!source) { + return expect(source).to.be.ok(); + } + const eventIds = (source?.[ALERT_ANCESTORS] as Ancestor[]) + .filter((event) => event.depth === 1) + .map((event) => event.id); + expect(source).eql({ + ...source, + agent: { + ephemeral_id: '1b4978a0-48be-49b1-ac96-323425b389ab', + hostname: 'zeek-sensor-amsterdam', + id: 'e52588e6-7aa3-4c89-a2c4-d6bc5c286db1', + type: 'auditbeat', + version: '8.0.0', + }, + auditd: { session: 'unset', summary: { actor: { primary: 'unset' } } }, + cloud: { instance: { id: '133551048' }, provider: 'digitalocean', region: 'ams3' }, + ecs: { version: '1.0.0-beta2' }, + [EVENT_KIND]: 'signal', + host: { + architecture: 'x86_64', + containerized: false, + hostname: 'zeek-sensor-amsterdam', + id: '2ce8b1e7d69e4a1d9c6bcddc473da9d9', + name: 'zeek-sensor-amsterdam', + os: { + codename: 'bionic', + family: 'debian', + kernel: '4.15.0-45-generic', + name: 'Ubuntu', + platform: 'ubuntu', + version: '18.04.2 LTS (Bionic Beaver)', + }, + }, + service: { type: 'auditd' }, + user: { audit: { id: 'unset' }, id: '0', name: 'root' }, + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DEPTH]: 2, + [ALERT_GROUP_ID]: source[ALERT_GROUP_ID], + [ALERT_REASON]: + 'event by root on zeek-sensor-amsterdam created high alert Signal Testing Query.', + [ALERT_RULE_UUID]: source[ALERT_RULE_UUID], + [ALERT_ANCESTORS]: [ + { + depth: 0, + id: 'VhXOBmkBR346wHgnLP8T', + index: 'auditbeat-8.0.0-2019.02.19-000001', + type: 'event', + }, + { + depth: 1, + id: eventIds[0], + index: '', + rule: source[ALERT_RULE_UUID], + type: 'signal', + }, + { + depth: 0, + id: '4hbXBmkBR346wHgn6fdp', + index: 'auditbeat-8.0.0-2019.02.19-000001', + type: 'event', + }, + { + depth: 1, + id: eventIds[1], + index: '', + rule: source[ALERT_RULE_UUID], + type: 'signal', + }, + ], + }); + }); + + it('generates up to max_signals with an EQL rule', async () => { + const maxSignals = 200; + const rule: EqlRuleCreateProps = { + ...getEqlRuleForSignalTesting(['auditbeat-*']), + query: 'sequence by host.name [any where true] [any where true]', + max_signals: maxSignals, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId, size: maxSignals * 5 }); + // For EQL rules, max_signals is the maximum number of detected sequences: each sequence has a building block + // alert for each event in the sequence, so max_signals=200 results in 400 building blocks in addition to + // 200 regular alerts + expect(previewAlerts.length).eql(maxSignals * 3); + const shellSignals = previewAlerts.filter((alert) => alert._source?.[ALERT_DEPTH] === 2); + const buildingBlocks = previewAlerts.filter((alert) => alert._source?.[ALERT_DEPTH] === 1); + expect(shellSignals.length).eql(maxSignals); + expect(buildingBlocks.length).eql(maxSignals * 2); + }); + + it('generates signals when an index name contains special characters to encode', async () => { + const rule: EqlRuleCreateProps = { + ...getEqlRuleForSignalTesting(['auditbeat-*', '']), + query: 'configuration where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"', + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).eql(1); + }); + + it('uses the provided filters', async () => { + const rule: EqlRuleCreateProps = { + ...getEqlRuleForSignalTesting(['auditbeat-*']), + query: 'any where true', + filters: [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'source.ip', + params: { + query: '46.148.18.163', + }, + }, + query: { + match_phrase: { + 'source.ip': '46.148.18.163', + }, + }, + }, + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'event.action', + params: { + query: 'error', + }, + }, + query: { + match_phrase: { + 'event.action': 'error', + }, + }, + }, + ], + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).eql(2); + }); + + describe('with host risk index', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk'); + }); + + it('should be enriched with host risk score', async () => { + const rule: EqlRuleCreateProps = { + ...getEqlRuleForSignalTesting(['auditbeat-*']), + query: 'configuration where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"', + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).eql(1); + const fullSignal = previewAlerts[0]._source; + if (!fullSignal) { + return expect(fullSignal).to.be.ok(); + } + expect(fullSignal?.host?.risk?.calculated_level).to.eql('Critical'); + expect(fullSignal?.host?.risk?.calculated_score_norm).to.eql(96); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/index.ts new file mode 100644 index 0000000000000..547e3a4706e3c --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - rule execution logic', function () { + loadTestFile(require.resolve('./eql')); + loadTestFile(require.resolve('./machine_learning')); + loadTestFile(require.resolve('./new_terms')); + loadTestFile(require.resolve('./query')); + loadTestFile(require.resolve('./saved_query')); + loadTestFile(require.resolve('./threat_match')); + loadTestFile(require.resolve('./threshold')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_ml.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts similarity index 69% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_ml.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts index e1a3d4f0796c4..0949d8255bed2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_ml.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts @@ -33,9 +33,14 @@ import { import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createRule, - createRuleWithExceptionEntries, deleteAllAlerts, + deleteSignalsIndex, + executeSetupModuleRequest, + forceStartDatafeeds, getOpenSignals, + getPreviewAlerts, + previewRule, + previewRuleWithExceptionEntries, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -47,7 +52,7 @@ export default ({ getService }: FtrProviderContext) => { const siemModule = 'security_linux_v3'; const mlJobId = 'v3_linux_anomalous_network_activity'; - const testRule: MachineLearningRuleCreateProps = { + const rule: MachineLearningRuleCreateProps = { name: 'Test ML rule', description: 'Test ML rule description', risk_score: 50, @@ -56,63 +61,32 @@ export default ({ getService }: FtrProviderContext) => { anomaly_threshold: 30, machine_learning_job_id: mlJobId, from: '1900-01-01T00:00:00.000Z', + rule_id: 'ml-rule-id', }; - async function executeSetupModuleRequest(module: string, rspCode: number) { - const { body } = await supertest - .post(`/api/ml/modules/setup/${module}`) - .set('kbn-xsrf', 'true') - .send({ - prefix: '', - groups: ['auditbeat'], - indexPatternName: 'auditbeat-*', - startDatafeed: false, - useDedicatedIndex: true, - applyToAllSpaces: true, - }) - .expect(rspCode); - - return body; - } - - async function forceStartDatafeeds(jobId: string, rspCode: number) { - const { body } = await supertest - .post(`/api/ml/jobs/force_start_datafeeds`) - .set('kbn-xsrf', 'true') - .send({ - datafeedIds: [`datafeed-${jobId}`], - start: new Date().getUTCMilliseconds(), - }) - .expect(rspCode); - - return body; - } - - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/125033 // FLAKY: https://github.com/elastic/kibana/issues/142993 - describe.skip('Generating signals from ml anomalies', () => { + describe.skip('Machine learning type rules', () => { before(async () => { // Order is critical here: auditbeat data must be loaded before attempting to start the ML job, // as the job looks for certain indices on start await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); - await executeSetupModuleRequest(siemModule, 200); - await forceStartDatafeeds(mlJobId, 200); + await executeSetupModuleRequest({ module: siemModule, rspCode: 200, supertest }); + await forceStartDatafeeds({ jobId: mlJobId, rspCode: 200, supertest }); await esArchiver.load('x-pack/test/functional/es_archives/security_solution/anomalies'); }); after(async () => { await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/anomalies'); - }); - - afterEach(async () => { + await deleteSignalsIndex(supertest, log); await deleteAllAlerts(supertest, log); }); + // First test creates a real rule - remaining tests use preview API it('should create 1 alert from ML rule when record meets anomaly_threshold', async () => { - const createdRule = await createRule(supertest, log, testRule); - const signalsOpen = await getOpenSignals(supertest, log, es, createdRule); - expect(signalsOpen.hits.hits.length).toBe(1); - const signal = signalsOpen.hits.hits[0]; + const createdRule = await createRule(supertest, log, rule); + const alerts = await getOpenSignals(supertest, log, es, createdRule); + expect(alerts.hits.hits.length).toBe(1); + const signal = alerts.hits.hits[0]; expect(signal._source).toEqual( expect.objectContaining({ @@ -162,7 +136,7 @@ export default ({ getService }: FtrProviderContext) => { required_fields: [], risk_score: 50, risk_score_mapping: [], - rule_id: createdRule.rule_id, + rule_id: 'ml-rule-id', setup: '', severity: 'critical', severity_mapping: [], @@ -185,13 +159,12 @@ export default ({ getService }: FtrProviderContext) => { }); it('should create 7 alerts from ML rule when records meet anomaly_threshold', async () => { - const rule: MachineLearningRuleCreateProps = { - ...testRule, - anomaly_threshold: 20, - }; - const createdRule = await createRule(supertest, log, rule); - const signalsOpen = await getOpenSignals(supertest, log, es, createdRule); - expect(signalsOpen.hits.hits.length).toBe(7); + const { previewId } = await previewRule({ + supertest, + rule: { ...rule, anomaly_threshold: 20 }, + }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).toBe(7); }); describe('with non-value list exception', () => { @@ -199,18 +172,23 @@ export default ({ getService }: FtrProviderContext) => { await deleteAllExceptions(supertest, log); }); it('generates no signals when an exception is added for an ML rule', async () => { - const createdRule = await createRuleWithExceptionEntries(supertest, log, testRule, [ - [ - { - field: 'host.name', - operator: 'included', - type: 'match', - value: 'mothra', - }, + const { previewId } = await previewRuleWithExceptionEntries({ + supertest, + log, + rule, + entries: [ + [ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'mothra', + }, + ], ], - ]); - const signalsOpen = await getOpenSignals(supertest, log, es, createdRule); - expect(signalsOpen.hits.hits.length).toBe(0); + }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).toBe(0); }); }); @@ -227,21 +205,26 @@ export default ({ getService }: FtrProviderContext) => { it('generates no signals when a value list exception is added for an ML rule', async () => { const valueListId = 'value-list-id'; await importFile(supertest, log, 'keyword', ['mothra'], valueListId); - const createdRule = await createRuleWithExceptionEntries(supertest, log, testRule, [ - [ - { - field: 'host.name', - operator: 'included', - type: 'list', - list: { - id: valueListId, - type: 'keyword', + const { previewId } = await previewRuleWithExceptionEntries({ + supertest, + log, + rule, + entries: [ + [ + { + field: 'host.name', + operator: 'included', + type: 'list', + list: { + id: valueListId, + type: 'keyword', + }, }, - }, + ], ], - ]); - const signalsOpen = await getOpenSignals(supertest, log, es, createdRule); - expect(signalsOpen.hits.hits.length).toBe(0); + }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).toBe(0); }); }); @@ -255,10 +238,10 @@ export default ({ getService }: FtrProviderContext) => { }); it('should be enriched with host risk score', async () => { - const createdRule = await createRule(supertest, log, testRule); - const signalsOpen = await getOpenSignals(supertest, log, es, createdRule); - expect(signalsOpen.hits.hits.length).toBe(1); - const fullSignal = signalsOpen.hits.hits[0]._source; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).toBe(1); + const fullSignal = previewAlerts[0]._source; expect(fullSignal?.host?.risk?.calculated_level).toBe('Low'); expect(fullSignal?.host?.risk?.calculated_score_norm).toBe(1); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/new_terms.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/new_terms.ts new file mode 100644 index 0000000000000..4bfbe92118599 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/new_terms.ts @@ -0,0 +1,385 @@ +/* + * 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 expect from '@kbn/expect'; + +import { NewTermsRuleCreateProps } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; +import { orderBy } from 'lodash'; +import { getCreateNewTermsRulesSchemaMock } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema/mocks'; +import { DetectionAlert } from '@kbn/security-solution-plugin/common/detection_engine/schemas/alerts'; +import { + createRule, + deleteAllAlerts, + deleteSignalsIndex, + getOpenSignals, + getPreviewAlerts, + previewRule, +} from '../../utils'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { previewRuleWithExceptionEntries } from '../../utils/preview_rule_with_exception_entries'; +import { deleteAllExceptions } from '../../../lists_api_integration/utils'; + +const removeRandomValuedProperties = (alert: DetectionAlert | undefined) => { + if (!alert) { + return undefined; + } + const { + 'kibana.version': version, + 'kibana.alert.rule.execution.uuid': execUuid, + 'kibana.alert.rule.uuid': uuid, + '@timestamp': timestamp, + 'kibana.alert.rule.created_at': createdAt, + 'kibana.alert.rule.updated_at': updatedAt, + 'kibana.alert.uuid': alertUuid, + ...restOfAlert + } = alert; + return restOfAlert; +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + const log = getService('log'); + + describe('New terms type rules', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + }); + + // First test creates a real rule - remaining tests use preview API + + // This test also tests that alerts are NOT created for terms that are not new: the host name + // suricata-sensor-san-francisco appears in a document at 2019-02-19T20:42:08.230Z, but also appears + // in earlier documents so is not new. An alert should not be generated for that term. + it('should generate 1 alert with 1 selected field', async () => { + const rule: NewTermsRuleCreateProps = { + ...getCreateNewTermsRulesSchemaMock('rule-1', true), + new_terms_fields: ['host.name'], + from: '2019-02-19T20:42:00.000Z', + history_window_start: '2019-01-19T20:42:00.000Z', + }; + + const createdRule = await createRule(supertest, log, rule); + const alerts = await getOpenSignals(supertest, log, es, createdRule); + + expect(alerts.hits.hits.length).eql(1); + expect(removeRandomValuedProperties(alerts.hits.hits[0]._source)).eql({ + 'kibana.alert.new_terms': ['zeek-newyork-sha-aa8df15'], + 'kibana.alert.rule.category': 'New Terms Rule', + 'kibana.alert.rule.consumer': 'siem', + 'kibana.alert.rule.name': 'Query with a rule id', + 'kibana.alert.rule.producer': 'siem', + 'kibana.alert.rule.rule_type_id': 'siem.newTermsRule', + 'kibana.space_ids': ['default'], + 'kibana.alert.rule.tags': [], + agent: { + ephemeral_id: '7cc2091a-72f1-4c63-843b-fdeb622f9c69', + hostname: 'zeek-newyork-sha-aa8df15', + id: '4b4462ef-93d2-409c-87a6-299d942e5047', + type: 'auditbeat', + version: '8.0.0', + }, + cloud: { instance: { id: '139865230' }, provider: 'digitalocean', region: 'nyc1' }, + ecs: { version: '1.0.0-beta2' }, + host: { + architecture: 'x86_64', + hostname: 'zeek-newyork-sha-aa8df15', + id: '3729d06ce9964aa98549f41cbd99334d', + ip: ['157.230.208.30', '10.10.0.6', 'fe80::24ce:f7ff:fede:a571'], + mac: ['26:ce:f7:de:a5:71'], + name: 'zeek-newyork-sha-aa8df15', + os: { + codename: 'cosmic', + family: 'debian', + kernel: '4.18.0-10-generic', + name: 'Ubuntu', + platform: 'ubuntu', + version: '18.10 (Cosmic Cuttlefish)', + }, + }, + message: + 'Login by user root (UID: 0) on pts/0 (PID: 20638) from 8.42.77.171 (IP: 8.42.77.171)', + process: { pid: 20638 }, + service: { type: 'system' }, + source: { ip: '8.42.77.171' }, + user: { id: 0, name: 'root', terminal: 'pts/0' }, + 'event.action': 'user_login', + 'event.category': 'authentication', + 'event.dataset': 'login', + 'event.kind': 'signal', + 'event.module': 'system', + 'event.origin': '/var/log/wtmp', + 'event.outcome': 'success', + 'event.type': 'authentication_success', + 'kibana.alert.original_time': '2019-02-19T20:42:08.230Z', + 'kibana.alert.ancestors': [ + { + id: 'x07wJ2oB9v5HJNSHhyxi', + type: 'event', + index: 'auditbeat-8.0.0-2019.02.19-000001', + depth: 0, + }, + ], + 'kibana.alert.status': 'active', + 'kibana.alert.workflow_status': 'open', + 'kibana.alert.depth': 1, + 'kibana.alert.reason': + 'authentication event with source 8.42.77.171 by root on zeek-newyork-sha-aa8df15 created high alert Query with a rule id.', + 'kibana.alert.severity': 'high', + 'kibana.alert.risk_score': 55, + 'kibana.alert.rule.parameters': { + description: 'Detecting root and admin users', + risk_score: 55, + severity: 'high', + author: [], + false_positives: [], + from: '2019-02-19T20:42:00.000Z', + rule_id: 'rule-1', + max_signals: 100, + risk_score_mapping: [], + severity_mapping: [], + threat: [], + to: 'now', + references: [], + version: 1, + exceptions_list: [], + immutable: false, + related_integrations: [], + required_fields: [], + setup: '', + type: 'new_terms', + query: '*', + new_terms_fields: ['host.name'], + history_window_start: '2019-01-19T20:42:00.000Z', + index: ['auditbeat-*'], + language: 'kuery', + }, + 'kibana.alert.rule.actions': [], + 'kibana.alert.rule.author': [], + 'kibana.alert.rule.created_by': 'elastic', + 'kibana.alert.rule.description': 'Detecting root and admin users', + 'kibana.alert.rule.enabled': true, + 'kibana.alert.rule.exceptions_list': [], + 'kibana.alert.rule.false_positives': [], + 'kibana.alert.rule.from': '2019-02-19T20:42:00.000Z', + 'kibana.alert.rule.immutable': false, + 'kibana.alert.rule.indices': ['auditbeat-*'], + 'kibana.alert.rule.interval': '5m', + 'kibana.alert.rule.max_signals': 100, + 'kibana.alert.rule.references': [], + 'kibana.alert.rule.risk_score_mapping': [], + 'kibana.alert.rule.rule_id': 'rule-1', + 'kibana.alert.rule.severity_mapping': [], + 'kibana.alert.rule.threat': [], + 'kibana.alert.rule.to': 'now', + 'kibana.alert.rule.type': 'new_terms', + 'kibana.alert.rule.updated_by': 'elastic', + 'kibana.alert.rule.version': 1, + 'kibana.alert.rule.risk_score': 55, + 'kibana.alert.rule.severity': 'high', + 'kibana.alert.original_event.action': 'user_login', + 'kibana.alert.original_event.category': 'authentication', + 'kibana.alert.original_event.dataset': 'login', + 'kibana.alert.original_event.kind': 'event', + 'kibana.alert.original_event.module': 'system', + 'kibana.alert.original_event.origin': '/var/log/wtmp', + 'kibana.alert.original_event.outcome': 'success', + 'kibana.alert.original_event.type': 'authentication_success', + }); + }); + + it('should generate 3 alerts when 1 document has 3 new values', async () => { + const rule: NewTermsRuleCreateProps = { + ...getCreateNewTermsRulesSchemaMock('rule-1', true), + new_terms_fields: ['host.ip'], + from: '2019-02-19T20:42:00.000Z', + history_window_start: '2019-01-19T20:42:00.000Z', + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + + expect(previewAlerts.length).eql(3); + const previewAlertsOrderedByHostIp = orderBy( + previewAlerts, + '_source.kibana.alert.new_terms', + 'asc' + ); + expect(previewAlertsOrderedByHostIp[0]._source?.['kibana.alert.new_terms']).eql([ + '10.10.0.6', + ]); + expect(previewAlertsOrderedByHostIp[1]._source?.['kibana.alert.new_terms']).eql([ + '157.230.208.30', + ]); + expect(previewAlertsOrderedByHostIp[2]._source?.['kibana.alert.new_terms']).eql([ + 'fe80::24ce:f7ff:fede:a571', + ]); + }); + + it('should generate alerts for every term when history window is small', async () => { + const rule: NewTermsRuleCreateProps = { + ...getCreateNewTermsRulesSchemaMock('rule-1', true), + new_terms_fields: ['host.name'], + from: '2019-02-19T20:42:00.000Z', + // Set the history_window_start close to 'from' so we should alert on all terms in the time range + history_window_start: '2019-02-19T20:41:59.000Z', + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + + expect(previewAlerts.length).eql(5); + const hostNames = previewAlerts + .map((signal) => signal._source?.['kibana.alert.new_terms']) + .sort(); + expect(hostNames[0]).eql(['suricata-sensor-amsterdam']); + expect(hostNames[1]).eql(['suricata-sensor-san-francisco']); + expect(hostNames[2]).eql(['zeek-newyork-sha-aa8df15']); + expect(hostNames[3]).eql(['zeek-sensor-amsterdam']); + expect(hostNames[4]).eql(['zeek-sensor-san-francisco']); + }); + + describe('timestamp override and fallback', () => { + before(async () => { + await esArchiver.load( + 'x-pack/test/functional/es_archives/security_solution/timestamp_fallback' + ); + await esArchiver.load( + 'x-pack/test/functional/es_archives/security_solution/timestamp_override_3' + ); + }); + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/timestamp_fallback' + ); + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/timestamp_override_3' + ); + }); + + it('should generate the correct alerts', async () => { + const rule: NewTermsRuleCreateProps = { + ...getCreateNewTermsRulesSchemaMock('rule-1', true), + // myfakeindex-3 does not have event.ingested mapped so we can test if the runtime field + // 'kibana.combined_timestamp' handles unmapped fields properly + index: ['timestamp-fallback-test', 'myfakeindex-3'], + new_terms_fields: ['host.name'], + from: '2020-12-16T16:00:00.000Z', + // Set the history_window_start close to 'from' so we should alert on all terms in the time range + history_window_start: '2020-12-16T15:59:00.000Z', + timestamp_override: 'event.ingested', + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + + expect(previewAlerts.length).eql(2); + const hostNames = previewAlerts + .map((signal) => signal._source?.['kibana.alert.new_terms']) + .sort(); + expect(hostNames[0]).eql(['host-3']); + expect(hostNames[1]).eql(['host-4']); + }); + }); + + describe('with exceptions', async () => { + afterEach(async () => { + await deleteAllExceptions(supertest, log); + }); + + it('should apply exceptions', async () => { + const rule: NewTermsRuleCreateProps = { + ...getCreateNewTermsRulesSchemaMock('rule-1', true), + new_terms_fields: ['host.name'], + from: '2019-02-19T20:42:00.000Z', + // Set the history_window_start close to 'from' so we should alert on all terms in the time range + history_window_start: '2019-02-19T20:41:59.000Z', + }; + + const { previewId } = await previewRuleWithExceptionEntries({ + supertest, + log, + rule, + entries: [ + [ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'zeek-sensor-san-francisco', + }, + ], + ], + }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + + expect(previewAlerts.length).eql(4); + const hostNames = previewAlerts + .map((signal) => signal._source?.['kibana.alert.new_terms']) + .sort(); + expect(hostNames[0]).eql(['suricata-sensor-amsterdam']); + expect(hostNames[1]).eql(['suricata-sensor-san-francisco']); + expect(hostNames[2]).eql(['zeek-newyork-sha-aa8df15']); + expect(hostNames[3]).eql(['zeek-sensor-amsterdam']); + }); + }); + + it('should work for max signals > 100', async () => { + const maxSignals = 200; + const rule: NewTermsRuleCreateProps = { + ...getCreateNewTermsRulesSchemaMock('rule-1', true), + new_terms_fields: ['process.pid'], + from: '2018-02-19T20:42:00.000Z', + // Set the history_window_start close to 'from' so we should alert on all terms in the time range + history_window_start: '2018-02-19T20:41:59.000Z', + max_signals: maxSignals, + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId, size: maxSignals * 2 }); + + expect(previewAlerts.length).eql(maxSignals); + const processPids = previewAlerts + .map((signal) => signal._source?.['kibana.alert.new_terms']) + .sort(); + expect(processPids[0]).eql([1]); + }); + + describe('alerts should be be enriched', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk'); + }); + + it('should be enriched with host risk score', async () => { + const rule: NewTermsRuleCreateProps = { + ...getCreateNewTermsRulesSchemaMock('rule-1', true), + new_terms_fields: ['host.name'], + from: '2019-02-19T20:42:00.000Z', + history_window_start: '2019-01-19T20:42:00.000Z', + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + + expect(previewAlerts[0]?._source?.host?.risk?.calculated_level).to.eql('Low'); + expect(previewAlerts[0]?._source?.host?.risk?.calculated_score_norm).to.eql(23); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts new file mode 100644 index 0000000000000..8090e4d2ce709 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts @@ -0,0 +1,426 @@ +/* + * 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 expect from '@kbn/expect'; +import { + ALERT_RISK_SCORE, + ALERT_RULE_PARAMETERS, + ALERT_RULE_RULE_ID, + ALERT_SEVERITY, + ALERT_WORKFLOW_STATUS, +} from '@kbn/rule-data-utils'; +import { flattenWithPrefix } from '@kbn/securitysolution-rules'; + +import { orderBy } from 'lodash'; + +import { QueryRuleCreateProps } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; +import { Ancestor } from '@kbn/security-solution-plugin/server/lib/detection_engine/signals/types'; +import { + ALERT_ANCESTORS, + ALERT_DEPTH, + ALERT_ORIGINAL_TIME, + ALERT_ORIGINAL_EVENT, +} from '@kbn/security-solution-plugin/common/field_maps/field_names'; +import { + createRule, + deleteAllAlerts, + deleteSignalsIndex, + getOpenSignals, + getPreviewAlerts, + getRuleForSignalTesting, + getSimpleRule, + previewRule, +} from '../../utils'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +/** + * Specific _id to use for some of the tests. If the archiver changes and you see errors + * here, update this to a new value of a chosen auditbeat record and update the tests values. + */ +const ID = 'BhbXBmkBR346wHgn4PeZ'; + +/** + * Test coverage: + * [x] - Happy path generating 1 alert + * [x] - Rule type respects max signals + * [x] - Alerts on alerts + */ + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + const log = getService('log'); + + describe('Query type rules', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/alerts/8.1.0'); + await esArchiver.load('x-pack/test/functional/es_archives/signals/severity_risk_overrides'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); + await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/alerts/8.1.0'); + await esArchiver.unload('x-pack/test/functional/es_archives/signals/severity_risk_overrides'); + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + }); + + // First test creates a real rule - remaining tests use preview API + it('should have the specific audit record for _id or none of these tests below will pass', async () => { + const rule: QueryRuleCreateProps = { + ...getRuleForSignalTesting(['auditbeat-*']), + query: `_id:${ID}`, + }; + const createdRule = await createRule(supertest, log, rule); + const alerts = await getOpenSignals(supertest, log, es, createdRule); + expect(alerts.hits.hits.length).greaterThan(0); + expect(alerts.hits.hits[0]._source?.['kibana.alert.ancestors'][0].id).eql(ID); + }); + + it('should abide by max_signals > 100', async () => { + const maxSignals = 200; + const rule: QueryRuleCreateProps = { + ...getRuleForSignalTesting(['auditbeat-*']), + max_signals: maxSignals, + }; + const { previewId } = await previewRule({ supertest, rule }); + // Search for 2x max_signals to make sure we aren't making more than max_signals + const previewAlerts = await getPreviewAlerts({ es, previewId, size: maxSignals * 2 }); + expect(previewAlerts.length).equal(maxSignals); + }); + + it('should have recorded the rule_id within the signal', async () => { + const rule: QueryRuleCreateProps = { + ...getRuleForSignalTesting(['auditbeat-*']), + query: `_id:${ID}`, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts[0]._source?.[ALERT_RULE_RULE_ID]).eql(getSimpleRule().rule_id); + }); + + it('should query and get back expected signal structure using a basic KQL query', async () => { + const rule: QueryRuleCreateProps = { + ...getRuleForSignalTesting(['auditbeat-*']), + query: `_id:${ID}`, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + const signal = previewAlerts[0]._source; + + expect(signal).eql({ + ...signal, + [ALERT_ANCESTORS]: [ + { + id: 'BhbXBmkBR346wHgn4PeZ', + type: 'event', + index: 'auditbeat-8.0.0-2019.02.19-000001', + depth: 0, + }, + ], + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DEPTH]: 1, + [ALERT_ORIGINAL_TIME]: '2019-02-19T17:40:03.790Z', + ...flattenWithPrefix(ALERT_ORIGINAL_EVENT, { + action: 'socket_closed', + dataset: 'socket', + kind: 'event', + module: 'system', + }), + }); + }); + + it('should query and get back expected signal structure when it is a signal on a signal', async () => { + const alertId = '30a75fe46d3dbdfab55982036f77a8d60e2d1112e96f277c3b8c22f9bb57817a'; + const rule: QueryRuleCreateProps = { + ...getRuleForSignalTesting([`.alerts-security.alerts-default*`]), + rule_id: 'signal-on-signal', + query: `_id:${alertId}`, + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + + expect(previewAlerts.length).to.eql(1); + + const signal = previewAlerts[0]._source; + + if (!signal) { + return expect(signal).to.be.ok(); + } + + expect(signal).eql({ + ...signal, + [ALERT_ANCESTORS]: [ + { + id: 'ahEToH8BK09aFtXZFVMq', + type: 'event', + index: 'events-index-000001', + depth: 0, + }, + { + rule: '031d5c00-a72f-11ec-a8a3-7b1c8077fc3e', + id: '30a75fe46d3dbdfab55982036f77a8d60e2d1112e96f277c3b8c22f9bb57817a', + type: 'signal', + index: '.internal.alerts-security.alerts-default-000001', + depth: 1, + }, + ], + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DEPTH]: 2, + [ALERT_ORIGINAL_TIME]: '2022-03-19T02:48:12.634Z', + ...flattenWithPrefix(ALERT_ORIGINAL_EVENT, { + agent_id_status: 'verified', + ingested: '2022-03-19T02:47:57.376Z', + dataset: 'elastic_agent.filebeat', + }), + }); + }); + + it('should not have risk score fields without risk indices', async () => { + const rule: QueryRuleCreateProps = { + ...getRuleForSignalTesting(['auditbeat-*']), + query: `_id:${ID}`, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts[0]?._source?.host?.risk).to.eql(undefined); + expect(previewAlerts[0]?._source?.user?.risk).to.eql(undefined); + }); + + describe('with host risk index', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk'); + }); + + it('should host have risk score field and do not have user risk score', async () => { + const rule: QueryRuleCreateProps = { + ...getRuleForSignalTesting(['auditbeat-*']), + query: `_id:${ID} or _id:GBbXBmkBR346wHgn5_eR or _id:x10zJ2oE9v5HJNSHhyxi`, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + + const firstAlert = previewAlerts.find( + (alert) => alert?._source?.host?.name === 'suricata-zeek-sensor-toronto' + ); + const secondAlert = previewAlerts.find( + (alert) => alert?._source?.host?.name === 'suricata-sensor-london' + ); + const thirdAlert = previewAlerts.find( + (alert) => alert?._source?.host?.name === 'IE11WIN8_1' + ); + + expect(firstAlert?._source?.host?.risk?.calculated_level).to.eql('Critical'); + expect(firstAlert?._source?.host?.risk?.calculated_score_norm).to.eql(96); + expect(firstAlert?._source?.user?.risk).to.eql(undefined); + expect(secondAlert?._source?.host?.risk?.calculated_level).to.eql('Low'); + expect(secondAlert?._source?.host?.risk?.calculated_score_norm).to.eql(20); + expect(thirdAlert?._source?.host?.risk).to.eql(undefined); + }); + }); + + describe('with host and user risk indices', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk'); + await esArchiver.load('x-pack/test/functional/es_archives/entity/user_risk'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk'); + await esArchiver.unload('x-pack/test/functional/es_archives/entity/user_risk'); + }); + + it('should have host and user risk score fields', async () => { + const rule: QueryRuleCreateProps = { + ...getRuleForSignalTesting(['auditbeat-*']), + query: `_id:${ID}`, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts[0]?._source?.host?.risk?.calculated_level).to.eql('Critical'); + expect(previewAlerts[0]?._source?.host?.risk?.calculated_score_norm).to.eql(96); + expect(previewAlerts[0]?._source?.user?.risk?.calculated_level).to.eql('Low'); + expect(previewAlerts[0]?._source?.user?.risk?.calculated_score_norm).to.eql(11); + }); + }); + + /** + * Here we test the functionality of Severity and Risk Score overrides (also called "mappings" + * in the code). If the rule specifies a mapping, then the final Severity or Risk Score + * value of the signal will be taken from the mapped field of the source event. + */ + it('should get default severity and risk score if there is no mapping', async () => { + const rule: QueryRuleCreateProps = { + ...getRuleForSignalTesting(['signal_overrides']), + severity: 'medium', + risk_score: 75, + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + + expect(previewAlerts.length).equal(4); + previewAlerts.forEach((alert) => { + expect(alert._source?.[ALERT_SEVERITY]).equal('medium'); + expect(alert._source?.[ALERT_RULE_PARAMETERS].severity_mapping).eql([]); + + expect(alert._source?.[ALERT_RISK_SCORE]).equal(75); + expect(alert._source?.[ALERT_RULE_PARAMETERS].risk_score_mapping).eql([]); + }); + }); + + it('should get overridden severity if the rule has a mapping for it', async () => { + const rule: QueryRuleCreateProps = { + ...getRuleForSignalTesting(['signal_overrides']), + severity: 'medium', + severity_mapping: [ + { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, + { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, + ], + risk_score: 75, + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + const alertsOrderedByParentId = orderBy(previewAlerts, 'signal.parent.id', 'asc'); + const severities = alertsOrderedByParentId.map((alert) => ({ + id: (alert._source?.[ALERT_ANCESTORS] as Ancestor[])[0].id, + value: alert._source?.[ALERT_SEVERITY], + })); + + expect(alertsOrderedByParentId.length).equal(4); + expect(severities).eql([ + { id: '1', value: 'high' }, + { id: '2', value: 'critical' }, + { id: '3', value: 'critical' }, + { id: '4', value: 'critical' }, + ]); + + alertsOrderedByParentId.forEach((alert) => { + expect(alert._source?.[ALERT_RISK_SCORE]).equal(75); + expect(alert._source?.[ALERT_RULE_PARAMETERS].risk_score_mapping).eql([]); + expect(alert._source?.[ALERT_RULE_PARAMETERS].severity_mapping).eql([ + { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, + { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, + ]); + }); + }); + + it('should get overridden risk score if the rule has a mapping for it', async () => { + const rule: QueryRuleCreateProps = { + ...getRuleForSignalTesting(['signal_overrides']), + severity: 'medium', + risk_score: 75, + risk_score_mapping: [ + { field: 'my_risk', operator: 'equals', value: '', risk_score: undefined }, + ], + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + const alertsOrderedByParentId = orderBy(previewAlerts, 'signal.parent.id', 'asc'); + const riskScores = alertsOrderedByParentId.map((alert) => ({ + id: (alert._source?.[ALERT_ANCESTORS] as Ancestor[])[0].id, + value: alert._source?.[ALERT_RISK_SCORE], + })); + + expect(alertsOrderedByParentId.length).equal(4); + expect(riskScores).eql([ + { id: '1', value: 31.14 }, + { id: '2', value: 32.14 }, + { id: '3', value: 33.14 }, + { id: '4', value: 34.14 }, + ]); + + alertsOrderedByParentId.forEach((alert) => { + expect(alert._source?.[ALERT_SEVERITY]).equal('medium'); + expect(alert._source?.[ALERT_RULE_PARAMETERS].severity_mapping).eql([]); + expect(alert._source?.[ALERT_RULE_PARAMETERS].risk_score_mapping).eql([ + { field: 'my_risk', operator: 'equals', value: '' }, + ]); + }); + }); + + it('should get overridden severity and risk score if the rule has both mappings', async () => { + const rule: QueryRuleCreateProps = { + ...getRuleForSignalTesting(['signal_overrides']), + severity: 'medium', + severity_mapping: [ + { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, + { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, + ], + risk_score: 75, + risk_score_mapping: [ + { field: 'my_risk', operator: 'equals', value: '', risk_score: undefined }, + ], + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + const alertsOrderedByParentId = orderBy(previewAlerts, 'signal.parent.id', 'asc'); + const values = alertsOrderedByParentId.map((alert) => ({ + id: (alert._source?.[ALERT_ANCESTORS] as Ancestor[])[0].id, + severity: alert._source?.[ALERT_SEVERITY], + risk: alert._source?.[ALERT_RISK_SCORE], + })); + + expect(alertsOrderedByParentId.length).equal(4); + expect(values).eql([ + { id: '1', severity: 'high', risk: 31.14 }, + { id: '2', severity: 'critical', risk: 32.14 }, + { id: '3', severity: 'critical', risk: 33.14 }, + { id: '4', severity: 'critical', risk: 34.14 }, + ]); + + alertsOrderedByParentId.forEach((alert) => { + expect(alert._source?.[ALERT_RULE_PARAMETERS].severity_mapping).eql([ + { field: 'my_severity', operator: 'equals', value: 'sev_900', severity: 'high' }, + { field: 'my_severity', operator: 'equals', value: 'sev_max', severity: 'critical' }, + ]); + expect(alert._source?.[ALERT_RULE_PARAMETERS].risk_score_mapping).eql([ + { field: 'my_risk', operator: 'equals', value: '' }, + ]); + }); + }); + + it('should generate signals with name_override field', async () => { + const rule: QueryRuleCreateProps = { + ...getRuleForSignalTesting(['auditbeat-*']), + query: `event.action:boot`, + rule_name_override: 'event.action', + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + const fullSignal = previewAlerts[0]; + if (!fullSignal) { + return expect(fullSignal).to.be.ok(); + } + + expect(previewAlerts[0]._source?.['kibana.alert.rule.name']).to.eql('boot'); + }); + + it('should not generate duplicate signals', async () => { + const rule: QueryRuleCreateProps = { + ...getRuleForSignalTesting(['auditbeat-*']), + query: `_id:${ID}`, + }; + + const { previewId } = await previewRule({ supertest, rule, invocationCount: 2 }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).to.eql(1); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/saved_query.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/saved_query.ts new file mode 100644 index 0000000000000..c6d26e994a99d --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/saved_query.ts @@ -0,0 +1,85 @@ +/* + * 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 expect from '@kbn/expect'; +import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; +import { flattenWithPrefix } from '@kbn/securitysolution-rules'; + +import { SavedQueryRuleCreateProps } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; +import { + ALERT_ANCESTORS, + ALERT_DEPTH, + ALERT_ORIGINAL_TIME, + ALERT_ORIGINAL_EVENT, +} from '@kbn/security-solution-plugin/common/field_maps/field_names'; +import { + createRule, + deleteAllAlerts, + deleteSignalsIndex, + getOpenSignals, + getRuleForSignalTesting, +} from '../../utils'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +/** + * Specific _id to use for some of the tests. If the archiver changes and you see errors + * here, update this to a new value of a chosen auditbeat record and update the tests values. + */ +const ID = 'BhbXBmkBR346wHgn4PeZ'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + const log = getService('log'); + + describe('Saved query type rules', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + }); + + // First test creates a real rule - remaining tests use preview API + it('should query and get back expected signal structure using a saved query rule', async () => { + const rule: SavedQueryRuleCreateProps = { + ...getRuleForSignalTesting(['auditbeat-*']), + type: 'saved_query', + query: `_id:${ID}`, + saved_id: 'doesnt-exist', + }; + const createdRule = await createRule(supertest, log, rule); + const alerts = await getOpenSignals(supertest, log, es, createdRule); + const signal = alerts.hits.hits[0]._source; + expect(signal).eql({ + ...signal, + [ALERT_ANCESTORS]: [ + { + id: 'BhbXBmkBR346wHgn4PeZ', + type: 'event', + index: 'auditbeat-8.0.0-2019.02.19-000001', + depth: 0, + }, + ], + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_DEPTH]: 1, + [ALERT_ORIGINAL_TIME]: '2019-02-19T17:40:03.790Z', + ...flattenWithPrefix(ALERT_ORIGINAL_EVENT, { + action: 'socket_closed', + dataset: 'socket', + kind: 'event', + module: 'system', + }), + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts new file mode 100644 index 0000000000000..dfa1f81f6c5d2 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts @@ -0,0 +1,1306 @@ +/* + * 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 { get, isEqual } from 'lodash'; +import expect from '@kbn/expect'; +import { + ALERT_REASON, + ALERT_RULE_UUID, + ALERT_STATUS, + ALERT_RULE_NAMESPACE, + ALERT_RULE_UPDATED_AT, + ALERT_UUID, + ALERT_WORKFLOW_STATUS, + SPACE_IDS, + VERSION, +} from '@kbn/rule-data-utils'; +import { flattenWithPrefix } from '@kbn/securitysolution-rules'; + +import { ThreatMatchRuleCreateProps } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; + +import { ENRICHMENT_TYPES } from '@kbn/security-solution-plugin/common/cti/constants'; +import { Ancestor } from '@kbn/security-solution-plugin/server/lib/detection_engine/signals/types'; +import { + ALERT_ANCESTORS, + ALERT_DEPTH, + ALERT_ORIGINAL_EVENT_ACTION, + ALERT_ORIGINAL_EVENT_CATEGORY, + ALERT_ORIGINAL_EVENT_MODULE, + ALERT_ORIGINAL_TIME, +} from '@kbn/security-solution-plugin/common/field_maps/field_names'; +import { + previewRule, + getOpenSignals, + getPreviewAlerts, + deleteSignalsIndex, + deleteAllAlerts, + createRule, +} from '../../utils'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +const format = (value: unknown): string => JSON.stringify(value, null, 2); + +// Asserts that each expected value is included in the subject, independent of +// ordering. Uses _.isEqual for value comparison. +const assertContains = (subject: unknown[], expected: unknown[]) => + expected.forEach((expectedValue) => + expect(subject.some((value) => isEqual(value, expectedValue))).to.eql( + true, + `expected ${format(subject)} to contain ${format(expectedValue)}` + ) + ); + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + const es = getService('es'); + const log = getService('log'); + + /** + * Specific api integration tests for threat matching rule type + */ + describe('Threat match type rules', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + }); + + // First test creates a real rule - remaining tests use preview API + it('should be able to execute and get 10 signals when doing a specific query', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip + threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity + threat_mapping: [ + // We match host.name against host.name + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const createdRule = await createRule(supertest, log, rule); + const alerts = await getOpenSignals(supertest, log, es, createdRule); + expect(alerts.hits.hits.length).equal(10); + const fullSource = alerts.hits.hits.find( + (signal) => + (signal._source?.[ALERT_ANCESTORS] as Ancestor[])[0].id === '7yJ-B2kBR346wHgnhlMn' + ); + const fullSignal = fullSource?._source; + if (!fullSignal) { + return expect(fullSignal).to.be.ok(); + } + expect(fullSignal).eql({ + ...fullSignal, + '@timestamp': fullSignal['@timestamp'], + agent: { + ephemeral_id: '1b4978a0-48be-49b1-ac96-323425b389ab', + hostname: 'zeek-sensor-amsterdam', + id: 'e52588e6-7aa3-4c89-a2c4-d6bc5c286db1', + type: 'auditbeat', + version: '8.0.0', + }, + auditd: { + data: { + hostname: '46.101.47.213', + op: 'PAM:bad_ident', + terminal: 'ssh', + }, + message_type: 'user_err', + result: 'fail', + sequence: 2267, + session: 'unset', + summary: { + actor: { + primary: 'unset', + secondary: 'root', + }, + how: '/usr/sbin/sshd', + object: { + primary: 'ssh', + secondary: '46.101.47.213', + type: 'user-session', + }, + }, + }, + cloud: { + instance: { + id: '133551048', + }, + provider: 'digitalocean', + region: 'ams3', + }, + ecs: { + version: '1.0.0-beta2', + }, + ...flattenWithPrefix('event', { + action: 'error', + category: 'user-login', + module: 'auditd', + kind: 'signal', + }), + host: { + architecture: 'x86_64', + containerized: false, + hostname: 'zeek-sensor-amsterdam', + id: '2ce8b1e7d69e4a1d9c6bcddc473da9d9', + name: 'zeek-sensor-amsterdam', + os: { + codename: 'bionic', + family: 'debian', + kernel: '4.15.0-45-generic', + name: 'Ubuntu', + platform: 'ubuntu', + version: '18.04.2 LTS (Bionic Beaver)', + }, + }, + network: { + direction: 'incoming', + }, + process: { + executable: '/usr/sbin/sshd', + pid: 32739, + }, + service: { + type: 'auditd', + }, + source: { + ip: '46.101.47.213', + }, + user: { + audit: { + id: 'unset', + }, + id: '0', + name: 'root', + }, + [ALERT_ANCESTORS]: [ + { + id: '7yJ-B2kBR346wHgnhlMn', + type: 'event', + index: 'auditbeat-8.0.0-2019.02.19-000001', + depth: 0, + }, + ], + [ALERT_DEPTH]: 1, + [ALERT_ORIGINAL_EVENT_ACTION]: 'error', + [ALERT_ORIGINAL_EVENT_CATEGORY]: 'user-login', + [ALERT_ORIGINAL_EVENT_MODULE]: 'auditd', + [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], + [ALERT_REASON]: + 'user-login event with source 46.101.47.213 by root on zeek-sensor-amsterdam created high alert Query with a rule id.', + [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], + [ALERT_STATUS]: 'active', + [ALERT_UUID]: fullSignal[ALERT_UUID], + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['default'], + [VERSION]: fullSignal[VERSION], + threat: { + enrichments: get(fullSignal, 'threat.enrichments'), + }, + ...flattenWithPrefix(ALERT_RULE_NAMESPACE, { + actions: [], + author: [], + category: 'Indicator Match Rule', + consumer: 'siem', + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + exceptions_list: [], + false_positives: [], + from: '1900-01-01T00:00:00.000Z', + immutable: false, + interval: '5m', + max_signals: 100, + name: 'Query with a rule id', + producer: 'siem', + references: [], + risk_score: 55, + risk_score_mapping: [], + rule_type_id: 'siem.indicatorRule', + severity: 'high', + severity_mapping: [], + tags: [], + threat: [], + to: 'now', + type: 'threat_match', + updated_at: fullSignal[ALERT_RULE_UPDATED_AT], + updated_by: 'elastic', + uuid: fullSignal[ALERT_RULE_UUID], + version: 1, + }), + }); + }); + + it('should return 0 matches if the mapping does not match against anything in the mapping', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip + threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity + threat_mapping: [ + // We match host.name against host.name + { + entries: [ + { + field: 'host.name', + value: 'invalid.mapping.value', // invalid mapping value + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).equal(0); + }); + + it('should return 0 signals when using an AND and one of the clauses does not have data', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip + threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity + threat_mapping: [ + { + entries: [ + { + field: 'source.ip', + value: 'source.ip', + type: 'mapping', + }, + { + field: 'source.ip', + value: 'destination.ip', // All records from the threat query do NOT have destination.ip, so those records that do not should drop this entire AND clause. + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).equal(0); + }); + + it('should return 0 signals when using an AND and one of the clauses has a made up value that does not exist', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + type: 'threat_match', + index: ['auditbeat-*'], + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip + threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity + threat_mapping: [ + { + entries: [ + { + field: 'source.ip', + value: 'source.ip', + type: 'mapping', + }, + { + field: 'source.ip', + value: 'made.up.non.existent.field', // made up field should not match + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).equal(0); + }); + + describe('timeout behavior', () => { + // TODO: unskip this and see if we can make it not flaky + it.skip('will return an error if a rule execution exceeds the rule interval', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a short interval', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: '*:*', // broad query to take more time + threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity + threat_mapping: [ + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + concurrent_searches: 1, + interval: '1s', // short interval + items_per_search: 1, // iterate only 1 threat item per loop to ensure we're slow + }; + + const { logs } = await previewRule({ supertest, rule }); + expect(logs[0].errors[0]).to.contain('execution has exceeded its allotted interval'); + }); + }); + + describe('indicator enrichment: threat-first search', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/filebeat/threat_intel'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/filebeat/threat_intel'); + }); + + it('enriches signals with the single indicator that matched', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: '*:*', // narrow events down to 2 with a destination.ip + threat_indicator_path: 'threat.indicator', + threat_query: 'threat.indicator.domain: 159.89.119.67', // narrow things down to indicators with a domain + threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module + threat_mapping: [ + { + entries: [ + { + value: 'threat.indicator.domain', + field: 'destination.ip', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).equal(2); + + const threats = previewAlerts.map((hit) => hit._source?.threat); + expect(threats).to.eql([ + { + enrichments: [ + { + feed: {}, + indicator: { + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + provider: 'geenensp', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, + type: 'url', + }, + matched: { + atomic: '159.89.119.67', + id: '978783', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'destination.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + ], + }, + { + enrichments: [ + { + feed: {}, + indicator: { + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + provider: 'geenensp', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, + type: 'url', + }, + matched: { + atomic: '159.89.119.67', + id: '978783', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'destination.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + ], + }, + ]); + }); + + it('enriches signals with multiple indicators if several matched', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: 'NOT source.port:35326', // specify query to have signals more than treat indicators, but only 1 will match + threat_indicator_path: 'threat.indicator', + threat_query: 'threat.indicator.ip: *', + threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module + threat_mapping: [ + { + entries: [ + { + value: 'threat.indicator.ip', + field: 'source.ip', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).equal(1); + + const [threat] = previewAlerts.map((hit) => hit._source?.threat) as Array<{ + enrichments: unknown[]; + }>; + + assertContains(threat.enrichments, [ + { + feed: {}, + indicator: { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + port: 57324, + provider: 'geenensp', + type: 'url', + }, + matched: { + atomic: '45.115.45.3', + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + { + feed: {}, + indicator: { + description: 'this should match auditbeat/hosts on ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + provider: 'other_provider', + type: 'ip', + }, + + matched: { + atomic: '45.115.45.3', + id: '978787', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + ]); + }); + + it('adds a single indicator that matched multiple fields', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: 'NOT source.port:35326', // specify query to have signals more than treat indicators, but only 1 will match + threat_indicator_path: 'threat.indicator', + threat_query: 'threat.indicator.port: 57324 or threat.indicator.ip:45.115.45.3', // narrow our query to a single indicator + threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module + threat_mapping: [ + { + entries: [ + { + value: 'threat.indicator.port', + field: 'source.port', + type: 'mapping', + }, + ], + }, + { + entries: [ + { + value: 'threat.indicator.ip', + field: 'source.ip', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).equal(1); + + const [threat] = previewAlerts.map((hit) => hit._source?.threat) as Array<{ + enrichments: unknown[]; + }>; + + assertContains(threat.enrichments, [ + { + feed: {}, + indicator: { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + port: 57324, + provider: 'geenensp', + type: 'url', + }, + matched: { + atomic: '45.115.45.3', + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + // We do not merge matched indicators during enrichment, so in + // certain circumstances a given indicator document could appear + // multiple times in an enriched alert (albeit with different + // threat.indicator.matched data). That's the case with the + // first and third indicators matched, here. + { + feed: {}, + indicator: { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + port: 57324, + provider: 'geenensp', + type: 'url', + }, + + matched: { + atomic: 57324, + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.port', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + { + feed: {}, + indicator: { + description: 'this should match auditbeat/hosts on ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + provider: 'other_provider', + type: 'ip', + }, + matched: { + atomic: '45.115.45.3', + id: '978787', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + ]); + }); + + it('generates multiple signals with multiple matches', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + threat_language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: '*:*', // narrow our query to a single record that matches two indicators + threat_indicator_path: 'threat.indicator', + threat_query: '*:*', + threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module + threat_mapping: [ + { + entries: [ + { + value: 'threat.indicator.port', + field: 'source.port', + type: 'mapping', + }, + { + value: 'threat.indicator.ip', + field: 'source.ip', + type: 'mapping', + }, + ], + }, + { + entries: [ + { + value: 'threat.indicator.domain', + field: 'destination.ip', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).equal(2); + + const threats = previewAlerts.map((hit) => hit._source?.threat) as Array<{ + enrichments: unknown[]; + }>; + + assertContains(threats[0].enrichments, [ + { + feed: {}, + indicator: { + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + provider: 'geenensp', + type: 'url', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, + }, + matched: { + atomic: '159.89.119.67', + id: '978783', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'destination.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + { + feed: {}, + indicator: { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + port: 57324, + provider: 'geenensp', + type: 'url', + }, + matched: { + atomic: '45.115.45.3', + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + { + feed: {}, + indicator: { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + port: 57324, + provider: 'geenensp', + type: 'url', + }, + matched: { + atomic: 57324, + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.port', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + ]); + + assertContains(threats[1].enrichments, [ + { + feed: {}, + indicator: { + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + provider: 'geenensp', + type: 'url', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, + }, + matched: { + atomic: '159.89.119.67', + id: '978783', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'destination.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + ]); + }); + }); + + describe('indicator enrichment: event-first search', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/filebeat/threat_intel'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/filebeat/threat_intel'); + }); + + it('enriches signals with the single indicator that matched', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: 'destination.ip:159.89.119.67', + threat_indicator_path: 'threat.indicator', + threat_query: 'threat.indicator.domain: *', // narrow things down to indicators with a domain + threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module + threat_mapping: [ + { + entries: [ + { + value: 'threat.indicator.domain', + field: 'destination.ip', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).equal(2); + + const threats = previewAlerts.map((hit) => hit._source?.threat); + expect(threats).to.eql([ + { + enrichments: [ + { + feed: {}, + indicator: { + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + provider: 'geenensp', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, + type: 'url', + }, + matched: { + atomic: '159.89.119.67', + id: '978783', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'destination.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + ], + }, + { + enrichments: [ + { + feed: {}, + indicator: { + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + provider: 'geenensp', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, + type: 'url', + }, + matched: { + atomic: '159.89.119.67', + id: '978783', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'destination.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + ], + }, + ]); + }); + + it('enriches signals with multiple indicators if several matched', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: 'source.port: 57324', // narrow our query to a single record that matches two indicators + threat_indicator_path: 'threat.indicator', + threat_query: 'threat.indicator.ip: *', + threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module + threat_mapping: [ + { + entries: [ + { + value: 'threat.indicator.ip', + field: 'source.ip', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).equal(1); + + const [threat] = previewAlerts.map((hit) => hit._source?.threat) as Array<{ + enrichments: unknown[]; + }>; + + assertContains(threat.enrichments, [ + { + feed: {}, + indicator: { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + port: 57324, + provider: 'geenensp', + type: 'url', + }, + matched: { + atomic: '45.115.45.3', + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + { + feed: {}, + indicator: { + description: 'this should match auditbeat/hosts on ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + provider: 'other_provider', + type: 'ip', + }, + + matched: { + atomic: '45.115.45.3', + id: '978787', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + ]); + }); + + it('adds a single indicator that matched multiple fields', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: 'source.port: 57324', // narrow our query to a single record that matches two indicators + threat_indicator_path: 'threat.indicator', + threat_query: 'threat.indicator.ip: *', + threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module + threat_mapping: [ + { + entries: [ + { + value: 'threat.indicator.port', + field: 'source.port', + type: 'mapping', + }, + ], + }, + { + entries: [ + { + value: 'threat.indicator.ip', + field: 'source.ip', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).equal(1); + + const [threat] = previewAlerts.map((hit) => hit._source?.threat) as Array<{ + enrichments: unknown[]; + }>; + + assertContains(threat.enrichments, [ + { + feed: {}, + indicator: { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + port: 57324, + provider: 'geenensp', + type: 'url', + }, + matched: { + atomic: '45.115.45.3', + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + // We do not merge matched indicators during enrichment, so in + // certain circumstances a given indicator document could appear + // multiple times in an enriched alert (albeit with different + // threat.indicator.matched data). That's the case with the + // first and third indicators matched, here. + { + feed: {}, + indicator: { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + port: 57324, + provider: 'geenensp', + type: 'url', + }, + + matched: { + atomic: 57324, + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.port', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + { + feed: {}, + indicator: { + description: 'this should match auditbeat/hosts on ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + provider: 'other_provider', + type: 'ip', + }, + matched: { + atomic: '45.115.45.3', + id: '978787', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + ]); + }); + + it('generates multiple signals with multiple matches', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + threat_language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: '(source.port:57324 and source.ip:45.115.45.3) or destination.ip:159.89.119.67', // narrow our query to a single record that matches two indicators + threat_indicator_path: 'threat.indicator', + threat_query: '*:*', + threat_index: ['filebeat-*'], // Mimics indicators from the filebeat MISP module + threat_mapping: [ + { + entries: [ + { + value: 'threat.indicator.port', + field: 'source.port', + type: 'mapping', + }, + { + value: 'threat.indicator.ip', + field: 'source.ip', + type: 'mapping', + }, + ], + }, + { + entries: [ + { + value: 'threat.indicator.domain', + field: 'destination.ip', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).equal(2); + + const threats = previewAlerts.map((hit) => hit._source?.threat) as Array<{ + enrichments: unknown[]; + }>; + + assertContains(threats[0].enrichments, [ + { + feed: {}, + indicator: { + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + provider: 'geenensp', + type: 'url', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, + }, + matched: { + atomic: '159.89.119.67', + id: '978783', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'destination.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + { + feed: {}, + indicator: { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + port: 57324, + provider: 'geenensp', + type: 'url', + }, + matched: { + atomic: '45.115.45.3', + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + { + feed: {}, + indicator: { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + port: 57324, + provider: 'geenensp', + type: 'url', + }, + matched: { + atomic: 57324, + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.port', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + ]); + + assertContains(threats[1].enrichments, [ + { + feed: {}, + indicator: { + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + provider: 'geenensp', + type: 'url', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, + }, + matched: { + atomic: '159.89.119.67', + id: '978783', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'destination.ip', + type: ENRICHMENT_TYPES.IndicatorMatchRule, + }, + }, + ]); + }); + }); + + describe('alerts should be enriched', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk'); + }); + + it('should be enriched with host risk score', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip + threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity + threat_mapping: [ + // We match host.name against host.name + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId, size: 100 }); + expect(previewAlerts.length).equal(88); + const fullSource = previewAlerts.find( + (signal) => + (signal._source?.[ALERT_ANCESTORS] as Ancestor[])[0].id === '7yJ-B2kBR346wHgnhlMn' + ); + const fullSignal = fullSource?._source; + if (!fullSignal) { + return expect(fullSignal).to.be.ok(); + } + + expect(fullSignal?.host?.risk?.calculated_level).to.eql('Critical'); + expect(fullSignal?.host?.risk?.calculated_score_norm).to.eql(70); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threshold.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threshold.ts new file mode 100644 index 0000000000000..e3294ae9a8156 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threshold.ts @@ -0,0 +1,385 @@ +/* + * 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 expect from '@kbn/expect'; +import { + ALERT_REASON, + ALERT_RULE_UUID, + ALERT_WORKFLOW_STATUS, + EVENT_KIND, +} from '@kbn/rule-data-utils'; + +import { ThresholdRuleCreateProps } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; +import { Ancestor } from '@kbn/security-solution-plugin/server/lib/detection_engine/signals/types'; +import { + ALERT_ANCESTORS, + ALERT_DEPTH, + ALERT_ORIGINAL_TIME, + ALERT_THRESHOLD_RESULT, +} from '@kbn/security-solution-plugin/common/field_maps/field_names'; +import { + createRule, + getOpenSignals, + getPreviewAlerts, + getThresholdRuleForSignalTesting, + previewRule, +} from '../../utils'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + const log = getService('log'); + + describe('Threshold type rules', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); + }); + + // First test creates a real rule - remaining tests use preview API + it('generates 1 signal from Threshold rules when threshold is met', async () => { + const rule: ThresholdRuleCreateProps = { + ...getThresholdRuleForSignalTesting(['auditbeat-*']), + threshold: { + field: ['host.id'], + value: 700, + }, + }; + const createdRule = await createRule(supertest, log, rule); + const alerts = await getOpenSignals(supertest, log, es, createdRule); + expect(alerts.hits.hits.length).eql(1); + const fullSignal = alerts.hits.hits[0]._source; + if (!fullSignal) { + return expect(fullSignal).to.be.ok(); + } + const eventIds = (fullSignal?.[ALERT_ANCESTORS] as Ancestor[]).map((event) => event.id); + expect(fullSignal).eql({ + ...fullSignal, + 'host.id': '8cc95778cce5407c809480e8e32ad76b', + [EVENT_KIND]: 'signal', + [ALERT_ANCESTORS]: [ + { + depth: 0, + id: eventIds[0], + index: 'auditbeat-*', + type: 'event', + }, + ], + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_REASON]: 'event created high alert Signal Testing Query.', + [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], + [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], + [ALERT_DEPTH]: 1, + [ALERT_THRESHOLD_RESULT]: { + terms: [ + { + field: 'host.id', + value: '8cc95778cce5407c809480e8e32ad76b', + }, + ], + count: 788, + from: '2019-02-19T07:12:05.332Z', + }, + }); + }); + + it('generates 2 signals from Threshold rules when threshold is met', async () => { + const rule: ThresholdRuleCreateProps = { + ...getThresholdRuleForSignalTesting(['auditbeat-*']), + threshold: { + field: 'host.id', + value: 100, + }, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).eql(2); + }); + + it('applies the provided query before bucketing ', async () => { + const rule: ThresholdRuleCreateProps = { + ...getThresholdRuleForSignalTesting(['auditbeat-*']), + query: 'host.id:"2ab45fc1c41e4c84bbd02202a7e5761f"', + threshold: { + field: 'process.name', + value: 21, + }, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).eql(1); + }); + + it('generates no signals from Threshold rules when threshold is met and cardinality is not met', async () => { + const rule: ThresholdRuleCreateProps = { + ...getThresholdRuleForSignalTesting(['auditbeat-*']), + threshold: { + field: 'host.id', + value: 100, + cardinality: [ + { + field: 'destination.ip', + value: 100, + }, + ], + }, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).eql(0); + }); + + it('generates no signals from Threshold rules when cardinality is met and threshold is not met', async () => { + const rule: ThresholdRuleCreateProps = { + ...getThresholdRuleForSignalTesting(['auditbeat-*']), + threshold: { + field: 'host.id', + value: 1000, + cardinality: [ + { + field: 'destination.ip', + value: 5, + }, + ], + }, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).eql(0); + }); + + it('generates signals from Threshold rules when threshold and cardinality are both met', async () => { + const rule: ThresholdRuleCreateProps = { + ...getThresholdRuleForSignalTesting(['auditbeat-*']), + threshold: { + field: 'host.id', + value: 100, + cardinality: [ + { + field: 'destination.ip', + value: 5, + }, + ], + }, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).eql(1); + const fullSignal = previewAlerts[0]._source; + if (!fullSignal) { + return expect(fullSignal).to.be.ok(); + } + const eventIds = (fullSignal?.[ALERT_ANCESTORS] as Ancestor[]).map((event) => event.id); + expect(fullSignal).eql({ + ...fullSignal, + 'host.id': '8cc95778cce5407c809480e8e32ad76b', + [EVENT_KIND]: 'signal', + [ALERT_ANCESTORS]: [ + { + depth: 0, + id: eventIds[0], + index: 'auditbeat-*', + type: 'event', + }, + ], + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_REASON]: `event created high alert Signal Testing Query.`, + [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], + [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], + [ALERT_DEPTH]: 1, + [ALERT_THRESHOLD_RESULT]: { + terms: [ + { + field: 'host.id', + value: '8cc95778cce5407c809480e8e32ad76b', + }, + ], + cardinality: [ + { + field: 'destination.ip', + value: 7, + }, + ], + count: 788, + from: '2019-02-19T07:12:05.332Z', + }, + }); + }); + + it('should not generate signals if only one field meets the threshold requirement', async () => { + const rule: ThresholdRuleCreateProps = { + ...getThresholdRuleForSignalTesting(['auditbeat-*']), + threshold: { + field: ['host.id', 'process.name'], + value: 22, + }, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).eql(0); + }); + + it('generates signals from Threshold rules when bucketing by multiple fields', async () => { + const rule: ThresholdRuleCreateProps = { + ...getThresholdRuleForSignalTesting(['auditbeat-*']), + threshold: { + field: ['host.id', 'process.name', 'event.module'], + value: 21, + }, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).eql(1); + const fullSignal = previewAlerts[0]._source; + if (!fullSignal) { + return expect(fullSignal).to.be.ok(); + } + const eventIds = (fullSignal[ALERT_ANCESTORS] as Ancestor[]).map((event) => event.id); + expect(fullSignal).eql({ + ...fullSignal, + 'event.module': 'system', + 'host.id': '2ab45fc1c41e4c84bbd02202a7e5761f', + 'process.name': 'sshd', + [EVENT_KIND]: 'signal', + [ALERT_ANCESTORS]: [ + { + depth: 0, + id: eventIds[0], + index: 'auditbeat-*', + type: 'event', + }, + ], + [ALERT_WORKFLOW_STATUS]: 'open', + [ALERT_REASON]: `event with process sshd, created high alert Signal Testing Query.`, + [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], + [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], + [ALERT_DEPTH]: 1, + [ALERT_THRESHOLD_RESULT]: { + terms: [ + { + field: 'host.id', + value: '2ab45fc1c41e4c84bbd02202a7e5761f', + }, + { + field: 'process.name', + value: 'sshd', + }, + { + field: 'event.module', + value: 'system', + }, + ], + count: 21, + from: '2019-02-19T20:22:03.561Z', + }, + }); + }); + + describe('Timestamp override and fallback', async () => { + before(async () => { + await esArchiver.load( + 'x-pack/test/functional/es_archives/security_solution/timestamp_fallback' + ); + }); + + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/timestamp_fallback' + ); + }); + + it('applies timestamp override when using single field', async () => { + const rule: ThresholdRuleCreateProps = { + ...getThresholdRuleForSignalTesting(['timestamp-fallback-test']), + threshold: { + field: 'host.name', + value: 1, + }, + timestamp_override: 'event.ingested', + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).eql(4); + + for (const hit of previewAlerts) { + const originalTime = hit._source?.[ALERT_ORIGINAL_TIME]; + const hostName = hit._source?.['host.name']; + if (hostName === 'host-1') { + expect(originalTime).eql('2020-12-16T15:15:18.570Z'); + } else if (hostName === 'host-2') { + expect(originalTime).eql('2020-12-16T15:16:18.570Z'); + } else if (hostName === 'host-3') { + expect(originalTime).eql('2020-12-16T16:15:18.570Z'); + } else { + expect(originalTime).eql('2020-12-16T16:16:18.570Z'); + } + } + }); + + it('applies timestamp override when using multiple fields', async () => { + const rule: ThresholdRuleCreateProps = { + ...getThresholdRuleForSignalTesting(['timestamp-fallback-test']), + threshold: { + field: ['host.name', 'source.ip'], + value: 1, + }, + timestamp_override: 'event.ingested', + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + expect(previewAlerts.length).eql(4); + + for (const hit of previewAlerts) { + const originalTime = hit._source?.[ALERT_ORIGINAL_TIME]; + const hostName = hit._source?.['host.name']; + if (hostName === 'host-1') { + expect(originalTime).eql('2020-12-16T15:15:18.570Z'); + } else if (hostName === 'host-2') { + expect(originalTime).eql('2020-12-16T15:16:18.570Z'); + } else if (hostName === 'host-3') { + expect(originalTime).eql('2020-12-16T16:15:18.570Z'); + } else { + expect(originalTime).eql('2020-12-16T16:16:18.570Z'); + } + } + }); + }); + + describe('with host risk index', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/entity/host_risk'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/entity/host_risk'); + }); + + it('should be enriched with host risk score', async () => { + const rule: ThresholdRuleCreateProps = { + ...getThresholdRuleForSignalTesting(['auditbeat-*']), + threshold: { + field: 'host.name', + value: 100, + }, + }; + const { previewId } = await previewRule({ supertest, rule }); + const previewAlerts = await getPreviewAlerts({ es, previewId }); + + expect(previewAlerts[0]?._source?.host?.risk?.calculated_level).to.eql('Low'); + expect(previewAlerts[0]?._source?.host?.risk?.calculated_score_norm).to.eql(20); + expect(previewAlerts[1]?._source?.host?.risk?.calculated_level).to.eql('Critical'); + expect(previewAlerts[1]?._source?.host?.risk?.calculated_score_norm).to.eql(96); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/get_preview_alerts.ts b/x-pack/test/detection_engine_api_integration/utils/get_preview_alerts.ts new file mode 100644 index 0000000000000..48682e6b1e8b0 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/get_preview_alerts.ts @@ -0,0 +1,45 @@ +/* + * 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 type { Client } from '@elastic/elasticsearch'; +import { ALERT_RULE_UUID } from '@kbn/rule-data-utils'; +import { DetectionAlert } from '@kbn/security-solution-plugin/common/detection_engine/schemas/alerts'; +import { RiskEnrichmentFields } from '@kbn/security-solution-plugin/server/lib/detection_engine/signals/enrichments/types'; +import { refreshIndex } from './refresh_index'; + +/** + * Refresh an index, making changes available to search. + * Useful for tests where we want to ensure that a rule does NOT create alerts, e.g. testing exceptions. + * @param es The ElasticSearch handle + */ +export const getPreviewAlerts = async ({ + es, + previewId, + size, +}: { + es: Client; + previewId: string; + size?: number; +}) => { + const index = '.preview.alerts-security.alerts-*'; + await refreshIndex(es, index); + const query = { + bool: { + filter: { + term: { + [ALERT_RULE_UUID]: previewId, + }, + }, + }, + }; + const result = await es.search({ + index, + size, + query, + }); + return result.hits.hits; +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/index.ts b/x-pack/test/detection_engine_api_integration/utils/index.ts index 866136f172f12..b686589addc09 100644 --- a/x-pack/test/detection_engine_api_integration/utils/index.ts +++ b/x-pack/test/detection_engine_api_integration/utils/index.ts @@ -41,6 +41,7 @@ export * from './get_legacy_action_so'; export * from './get_legacy_actions_so_by_id'; export * from './get_open_signals'; export * from './get_prepackaged_rule_status'; +export * from './get_preview_alerts'; export * from './get_query_all_signals'; export * from './get_query_signal_ids'; export * from './get_query_signals_ids'; @@ -79,6 +80,9 @@ export * from './get_slack_action'; export * from './get_web_hook_action'; export * from './index_event_log_execution_events'; export * from './install_prepackaged_rules'; +export * from './machine_learning_setup'; +export * from './preview_rule_with_exception_entries'; +export * from './preview_rule'; export * from './refresh_index'; export * from './remove_time_fields_from_telemetry_stats'; export * from './remove_server_generated_properties'; diff --git a/x-pack/test/detection_engine_api_integration/utils/machine_learning_setup.ts b/x-pack/test/detection_engine_api_integration/utils/machine_learning_setup.ts new file mode 100644 index 0000000000000..b376df9407c4b --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/machine_learning_setup.ts @@ -0,0 +1,54 @@ +/* + * 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 type SuperTest from 'supertest'; + +export const executeSetupModuleRequest = async ({ + module, + rspCode, + supertest, +}: { + module: string; + rspCode: number; + supertest: SuperTest.SuperTest; +}) => { + const { body } = await supertest + .post(`/api/ml/modules/setup/${module}`) + .set('kbn-xsrf', 'true') + .send({ + prefix: '', + groups: ['auditbeat'], + indexPatternName: 'auditbeat-*', + startDatafeed: false, + useDedicatedIndex: true, + applyToAllSpaces: true, + }) + .expect(rspCode); + + return body; +}; + +export const forceStartDatafeeds = async ({ + jobId, + rspCode, + supertest, +}: { + jobId: string; + rspCode: number; + supertest: SuperTest.SuperTest; +}) => { + const { body } = await supertest + .post(`/api/ml/jobs/force_start_datafeeds`) + .set('kbn-xsrf', 'true') + .send({ + datafeedIds: [`datafeed-${jobId}`], + start: new Date().getUTCMilliseconds(), + }) + .expect(rspCode); + + return body; +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/preview_rule.ts b/x-pack/test/detection_engine_api_integration/utils/preview_rule.ts new file mode 100644 index 0000000000000..1360209d9a175 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/preview_rule.ts @@ -0,0 +1,50 @@ +/* + * 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 type SuperTest from 'supertest'; +import type { + RuleCreateProps, + PreviewRulesSchema, + RulePreviewLogs, +} from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; + +import { DETECTION_ENGINE_RULES_PREVIEW } from '@kbn/security-solution-plugin/common/constants'; + +/** + * Runs the preview for a rule. Any generated alerts will be written to .preview.alerts. + * This is much faster than actually running the rule, and can also quickly simulate multiple + * consecutive rule runs, e.g. for ensuring that rule state is properly handled across runs. + * @param supertest The supertest deps + * @param rule The rule to create + */ +export const previewRule = async ({ + supertest, + rule, + invocationCount = 1, + timeframeEnd = new Date(), +}: { + supertest: SuperTest.SuperTest; + rule: RuleCreateProps; + invocationCount?: number; + timeframeEnd?: Date; +}): Promise<{ + previewId: string; + logs: RulePreviewLogs[]; + isAborted: boolean; +}> => { + const previewRequest: PreviewRulesSchema = { + ...rule, + invocationCount, + timeframeEnd: timeframeEnd.toISOString(), + }; + const response = await supertest + .post(DETECTION_ENGINE_RULES_PREVIEW) + .set('kbn-xsrf', 'true') + .send(previewRequest) + .expect(200); + return response.body; +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/preview_rule_with_exception_entries.ts b/x-pack/test/detection_engine_api_integration/utils/preview_rule_with_exception_entries.ts new file mode 100644 index 0000000000000..efd5c71ac7047 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/preview_rule_with_exception_entries.ts @@ -0,0 +1,63 @@ +/* + * 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 type { ToolingLog } from '@kbn/tooling-log'; +import type SuperTest from 'supertest'; +import type { NonEmptyEntriesArray, OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; +import type { RuleCreateProps } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; + +import { createContainerWithEntries } from './create_container_with_entries'; +import { createContainerWithEndpointEntries } from './create_container_with_endpoint_entries'; +import { previewRule } from './preview_rule'; + +/** + * Convenience testing function where you can pass in just the entries and you will + * get a rule created with the entries added to an exception list and exception list item + * all auto-created at once. + * @param supertest super test agent + * @param rule The rule to create and attach an exception list to + * @param entries The entries to create the rule and exception list from + * @param endpointEntries The endpoint entries to create the rule and exception list from + * @param osTypes The os types to optionally add or not to add to the container + */ +export const previewRuleWithExceptionEntries = async ({ + supertest, + log, + rule, + entries, + endpointEntries, + invocationCount, + timeframeEnd, +}: { + supertest: SuperTest.SuperTest; + log: ToolingLog; + rule: RuleCreateProps; + entries: NonEmptyEntriesArray[]; + endpointEntries?: Array<{ + entries: NonEmptyEntriesArray; + osTypes: OsTypeArray | undefined; + }>; + invocationCount?: number; + timeframeEnd?: Date; +}) => { + const maybeExceptionList = await createContainerWithEntries(supertest, log, entries); + const maybeEndpointList = await createContainerWithEndpointEntries( + supertest, + log, + endpointEntries ?? [] + ); + + return previewRule({ + supertest, + rule: { + ...rule, + exceptions_list: [...maybeExceptionList, ...maybeEndpointList], + }, + invocationCount, + timeframeEnd, + }); +}; diff --git a/x-pack/test/functional/es_archives/security_solution/alerts/8.0.0/data.json.gz b/x-pack/test/functional/es_archives/security_solution/alerts/8.0.0/data.json.gz index 26952621f10e4bce828d8cb5e86504a8715be539..6a7559434f703e7ed3f292918c5c2212de9d5aed 100644 GIT binary patch literal 9203 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 XdYx?f%IOv|-!J2LtHp!q2yp%bp5ZPj literal 9231 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 DK?YAz diff --git a/x-pack/test/functional/es_archives/security_solution/alerts/8.0.0/mappings.json.gz b/x-pack/test/functional/es_archives/security_solution/alerts/8.0.0/mappings.json.gz index 3a26e140e7eaa87becd588b84b6cfae9955a2905..5a7347bcb63de38d68f5ca61618dc75ddd283901 100644 GIT binary patch literal 9609 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 literal 9711 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 CHeI6t literal 9500 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 GVgdjy(vj}~ literal 9739 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 XQ{nMN`SpJE1#Y-!$WCVl0rr0YQDEFA diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts index 2785fece6d90a..b5c72abac6329 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts @@ -19,7 +19,6 @@ import { waitForSignalsToBePresent, waitForRuleSuccessOrStatus, } from '../../../../detection_engine_api_integration/utils'; -import { ID } from '../../../../detection_engine_api_integration/security_and_spaces/group1/generating_signals'; import { obsOnlySpacesAllEsRead, obsOnlySpacesAll, @@ -31,6 +30,8 @@ type RuleRegistrySearchResponseWithErrors = RuleRegistrySearchResponse & { message: string; }; +const ID = 'BhbXBmkBR346wHgn4PeZ'; + // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver');