mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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
This commit is contained in:
parent
f402274270
commit
21c7f5e074
31 changed files with 3542 additions and 3500 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}`,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -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'));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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.
|
|
@ -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('.')],
|
||||
};
|
||||
}
|
|
@ -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-*', '<my-index-{now/d}*>']),
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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'));
|
||||
});
|
||||
};
|
|
@ -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);
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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',
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
File diff suppressed because it is too large
Load diff
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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<DetectionAlert & RiskEnrichmentFields>({
|
||||
index,
|
||||
size,
|
||||
query,
|
||||
});
|
||||
return result.hits.hits;
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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<SuperTest.Test>;
|
||||
}) => {
|
||||
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<SuperTest.Test>;
|
||||
}) => {
|
||||
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;
|
||||
};
|
|
@ -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<SuperTest.Test>;
|
||||
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;
|
||||
};
|
|
@ -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<SuperTest.Test>;
|
||||
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,
|
||||
});
|
||||
};
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue