[Security Solution][Detections] Add API integration tests for threshold and EQL rules (#97336)

* Add API integration tests for threshold rules and more tests for EQL rules

* Add API more tests for exceptions and value list exceptions

* Fix unit test and add EQL api test checking multiple signal generation

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marshall Main 2021-04-19 16:10:59 -04:00 committed by GitHub
parent 7927923f0e
commit a90afbf1ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 786 additions and 79 deletions

View file

@ -1161,8 +1161,8 @@ describe('get_filter', () => {
expect(request).toEqual({
method: 'POST',
path: `/testindex1,testindex2/_eql/search?allow_no_indices=true`,
event_category_field: 'event.other_category',
body: {
event_category_field: 'event.other_category',
size: 100,
query: 'process where true',
filter: {

View file

@ -65,7 +65,6 @@ interface EqlSearchRequest {
method: string;
path: string;
body: object;
event_category_field?: string;
}
export const buildEqlSearchRequest = (
@ -109,7 +108,7 @@ export const buildEqlSearchRequest = (
},
});
}
const baseRequest = {
return {
method: 'POST',
path: `/${indexString}/_eql/search?allow_no_indices=true`,
body: {
@ -120,14 +119,7 @@ export const buildEqlSearchRequest = (
filter: requestFilter,
},
},
event_category_field: eventCategoryOverride,
},
};
if (eventCategoryOverride) {
return {
...baseRequest,
event_category_field: eventCategoryOverride,
};
} else {
return baseRequest;
}
};

View file

@ -8,9 +8,20 @@
/* eslint-disable @typescript-eslint/naming-convention */
import expect from '@kbn/expect';
import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request';
import {
CreateRulesSchema,
EqlCreateSchema,
QueryCreateSchema,
ThreatMatchCreateSchema,
ThresholdCreateSchema,
} from '../../../../plugins/security_solution/common/detection_engine/schemas/request';
import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock';
import { deleteAllExceptions } from '../../../lists_api_integration/utils';
import {
createListsIndex,
deleteAllExceptions,
deleteListsIndex,
importFile,
} from '../../../lists_api_integration/utils';
import { RulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/response';
import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock';
import { CreateExceptionListItemSchema } from '../../../../plugins/lists/common';
@ -39,6 +50,9 @@ import {
getSignalsByIds,
findImmutableRuleById,
getPrePackagedRulesStatus,
getRuleForSignalTesting,
getOpenSignals,
createRuleWithExceptionEntries,
} from '../../utils';
import { ROLES } from '../../../../plugins/security_solution/common/test';
import { createUserAndRole, deleteUserAndRole } from '../roles_users_utils';
@ -576,25 +590,7 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should be able to execute against an exception list that does include valid entries and get back 0 signals', async () => {
const { id, list_id, namespace_type, type } = await createExceptionList(
supertest,
getCreateExceptionListMinimalSchemaMock()
);
const exceptionListItem: CreateExceptionListItemSchema = {
...getCreateExceptionListItemMinimalSchemaMock(),
entries: [
{
field: 'host.name', // This matches the query below which will exclude everything
operator: 'included',
type: 'match',
value: 'suricata-sensor-amsterdam',
},
],
};
await createExceptionListItem(supertest, exceptionListItem);
const ruleWithException: CreateRulesSchema = {
const rule: QueryCreateSchema = {
name: 'Simple Rule Query',
description: 'Simple Rule Query',
enabled: true,
@ -605,20 +601,200 @@ export default ({ getService }: FtrProviderContext) => {
type: 'query',
from: '1900-01-01T00:00:00.000Z',
query: 'host.name: "suricata-sensor-amsterdam"',
exceptions_list: [
};
const createdRule = await createRuleWithExceptionEntries(supertest, rule, [
[
{
id,
list_id,
namespace_type,
type,
field: 'host.name', // This matches the query above which will exclude everything
operator: 'included',
type: 'match',
value: 'suricata-sensor-amsterdam',
},
],
};
const rule = await createRule(supertest, ruleWithException);
await waitForRuleSuccessOrStatus(supertest, rule.id);
const signalsOpen = await getSignalsByIds(supertest, [rule.id]);
]);
const signalsOpen = await getOpenSignals(supertest, es, createdRule);
expect(signalsOpen.hits.hits.length).equal(0);
});
it('generates no signals when an exception is added for an EQL rule', async () => {
const rule: EqlCreateSchema = {
...getRuleForSignalTesting(['auditbeat-*']),
rule_id: 'eql-rule',
type: 'eql',
language: 'eql',
query: 'configuration where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"',
};
const createdRule = await createRuleWithExceptionEntries(supertest, rule, [
[
{
field: 'host.id',
operator: 'included',
type: 'match',
value: '8cc95778cce5407c809480e8e32ad76b',
},
],
]);
const signalsOpen = await getOpenSignals(supertest, es, createdRule);
expect(signalsOpen.hits.hits.length).equal(0);
});
it('generates no signals when an exception is added for a threshold rule', async () => {
const rule: ThresholdCreateSchema = {
...getRuleForSignalTesting(['auditbeat-*']),
rule_id: 'threshold-rule',
type: 'threshold',
language: 'kuery',
query: '*:*',
threshold: {
field: 'host.id',
value: 700,
},
};
const createdRule = await createRuleWithExceptionEntries(supertest, rule, [
[
{
field: 'host.id',
operator: 'included',
type: 'match',
value: '8cc95778cce5407c809480e8e32ad76b',
},
],
]);
const signalsOpen = await getOpenSignals(supertest, es, createdRule);
expect(signalsOpen.hits.hits.length).equal(0);
});
it('generates no signals when an exception is added for a threat match rule', async () => {
const rule: ThreatMatchCreateSchema = {
description: 'Detecting root and admin users',
name: 'Query with a rule id',
severity: 'high',
index: ['auditbeat-*'],
type: 'threat_match',
risk_score: 55,
language: 'kuery',
rule_id: 'rule-1',
from: '1900-01-01T00:00:00.000Z',
query: '*:*',
threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip
threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity
threat_mapping: [
// We match host.name against host.name
{
entries: [
{
field: 'host.name',
value: 'host.name',
type: 'mapping',
},
],
},
],
threat_filters: [],
};
const createdRule = await createRuleWithExceptionEntries(supertest, rule, [
[
{
field: 'source.ip',
operator: 'included',
type: 'match',
value: '188.166.120.93',
},
],
]);
const signalsOpen = await getOpenSignals(supertest, es, createdRule);
expect(signalsOpen.hits.hits.length).equal(0);
});
describe('rules with value list exceptions', () => {
beforeEach(async () => {
await createListsIndex(supertest);
});
afterEach(async () => {
await deleteListsIndex(supertest);
});
it('generates no signals when a value list exception is added for a query rule', async () => {
const valueListId = 'value-list-id';
await importFile(supertest, 'keyword', ['suricata-sensor-amsterdam'], valueListId);
const rule: QueryCreateSchema = {
name: 'Simple Rule Query',
description: 'Simple Rule Query',
enabled: true,
risk_score: 1,
rule_id: 'rule-1',
severity: 'high',
index: ['auditbeat-*'],
type: 'query',
from: '1900-01-01T00:00:00.000Z',
query: 'host.name: "suricata-sensor-amsterdam"',
};
const createdRule = await createRuleWithExceptionEntries(supertest, rule, [
[
{
field: 'host.name',
operator: 'included',
type: 'list',
list: {
id: valueListId,
type: 'keyword',
},
},
],
]);
const signalsOpen = await getOpenSignals(supertest, es, createdRule);
expect(signalsOpen.hits.hits.length).equal(0);
});
it('generates no signals when a value list exception is added for a threat match rule', async () => {
const valueListId = 'value-list-id';
await importFile(supertest, 'keyword', ['zeek-sensor-amsterdam'], valueListId);
const rule: ThreatMatchCreateSchema = {
description: 'Detecting root and admin users',
name: 'Query with a rule id',
severity: 'high',
index: ['auditbeat-*'],
type: 'threat_match',
risk_score: 55,
language: 'kuery',
rule_id: 'rule-1',
from: '1900-01-01T00:00:00.000Z',
query: '*:*',
threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip
threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity
threat_mapping: [
// We match host.name against host.name
{
entries: [
{
field: 'host.name',
value: 'host.name',
type: 'mapping',
},
],
},
],
threat_filters: [],
};
const createdRule = await createRuleWithExceptionEntries(supertest, rule, [
[
{
field: 'host.name',
operator: 'included',
type: 'list',
list: {
id: valueListId,
type: 'keyword',
},
},
],
]);
const signalsOpen = await getOpenSignals(supertest, es, createdRule);
expect(signalsOpen.hits.hits.length).equal(0);
});
});
});
});
});

View file

@ -6,11 +6,12 @@
*/
import expect from '@kbn/expect';
import { orderBy } from 'lodash';
import { orderBy, get } from 'lodash';
import {
EqlCreateSchema,
QueryCreateSchema,
ThresholdCreateSchema,
} from '../../../../plugins/security_solution/common/detection_engine/schemas/request';
import { DEFAULT_SIGNALS_INDEX } from '../../../../plugins/security_solution/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@ -216,55 +217,263 @@ export default ({ getService }: FtrProviderContext) => {
});
describe('EQL Rules', () => {
it('generates signals from EQL sequences in the expected form', async () => {
it('generates a correctly formatted signal from EQL non-sequence queries', async () => {
const rule: EqlCreateSchema = {
...getRuleForSignalTesting(['auditbeat-*']),
rule_id: 'eql-rule',
type: 'eql',
language: 'eql',
query: 'sequence by host.name [any where true] [any where true]',
query: 'configuration where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"',
};
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 1, [id]);
const signals = await getSignalsByRuleIds(supertest, ['eql-rule']);
const signal = signals.hits.hits[0]._source.signal;
expect(signals.hits.hits.length).eql(1);
const fullSignal = signals.hits.hits[0]._source;
expect(signal).eql({
rule: signal.rule,
group: signal.group,
original_time: signal.original_time,
status: 'open',
depth: 1,
ancestors: [
{
depth: 0,
id: 'gCF0B2kBR346wHgnb7m0',
index: 'auditbeat-8.0.0-2019.02.19-000001',
type: 'event',
expect(fullSignal).eql({
'@timestamp': fullSignal['@timestamp'],
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',
},
],
original_event: {
action: 'error',
category: 'user-login',
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',
},
event: {
action: 'changed-audit-configuration',
category: 'configuration',
module: 'auditd',
kind: 'signal',
},
parent: {
depth: 0,
id: 'gCF0B2kBR346wHgnb7m0',
index: 'auditbeat-8.0.0-2019.02.19-000001',
type: 'event',
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)',
},
},
parents: [
{
service: {
type: 'auditd',
},
user: {
audit: {
id: 'unset',
},
},
signal: {
rule: fullSignal.signal.rule,
original_time: fullSignal.signal.original_time,
status: 'open',
depth: 1,
ancestors: [
{
depth: 0,
id: '9xbRBmkBR346wHgngz2D',
index: 'auditbeat-8.0.0-2019.02.19-000001',
type: 'event',
},
],
original_event: {
action: 'changed-audit-configuration',
category: 'configuration',
module: 'auditd',
},
parent: {
depth: 0,
id: 'gCF0B2kBR346wHgnb7m0',
id: '9xbRBmkBR346wHgngz2D',
index: 'auditbeat-8.0.0-2019.02.19-000001',
type: 'event',
},
],
_meta: {
version: SIGNALS_TEMPLATE_VERSION,
parents: [
{
depth: 0,
id: '9xbRBmkBR346wHgngz2D',
index: 'auditbeat-8.0.0-2019.02.19-000001',
type: 'event',
},
],
_meta: {
version: SIGNALS_TEMPLATE_VERSION,
},
},
});
});
it('generates up to max_signals for non-sequence EQL queries', async () => {
const rule: EqlCreateSchema = {
...getRuleForSignalTesting(['auditbeat-*']),
rule_id: 'eql-rule',
type: 'eql',
language: 'eql',
query: 'any where true',
};
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 100, [id]);
const signals = await getSignalsByIds(supertest, [id], 1000);
const filteredSignals = signals.hits.hits.filter(
(signal) => signal._source.signal.depth === 1
);
expect(filteredSignals.length).eql(100);
});
it('uses the provided event_category_override', async () => {
const rule: EqlCreateSchema = {
...getRuleForSignalTesting(['auditbeat-*']),
rule_id: 'eql-rule',
type: 'eql',
language: 'eql',
query: 'config_change where agent.id=="a1d7b39c-f898-4dbe-a761-efb61939302d"',
event_category_override: 'auditd.message_type',
};
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 1, [id]);
const signals = await getSignalsByRuleIds(supertest, ['eql-rule']);
expect(signals.hits.hits.length).eql(1);
const fullSignal = signals.hits.hits[0]._source;
expect(fullSignal).eql({
'@timestamp': fullSignal['@timestamp'],
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',
},
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',
},
},
signal: {
rule: fullSignal.signal.rule,
original_time: fullSignal.signal.original_time,
status: 'open',
depth: 1,
ancestors: [
{
depth: 0,
id: '9xbRBmkBR346wHgngz2D',
index: 'auditbeat-8.0.0-2019.02.19-000001',
type: 'event',
},
],
original_event: {
action: 'changed-audit-configuration',
category: 'configuration',
module: 'auditd',
},
parent: {
depth: 0,
id: '9xbRBmkBR346wHgngz2D',
index: 'auditbeat-8.0.0-2019.02.19-000001',
type: 'event',
},
parents: [
{
depth: 0,
id: '9xbRBmkBR346wHgngz2D',
index: 'auditbeat-8.0.0-2019.02.19-000001',
type: 'event',
},
],
_meta: {
version: SIGNALS_TEMPLATE_VERSION,
},
},
});
});
@ -275,18 +484,76 @@ export default ({ getService }: FtrProviderContext) => {
rule_id: 'eql-rule',
type: 'eql',
language: 'eql',
query: 'sequence by host.name [any where true] [any where true]',
query: 'sequence by host.name [anomoly where true] [any where true]',
};
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 10, [id]);
await waitForSignalsToBePresent(supertest, 3, [id]);
const signals = await getSignalsByRuleIds(supertest, ['eql-rule']);
const buildingBlock = signals.hits.hits.find(
(signal) =>
signal._source.signal.depth === 1 &&
get(signal._source, 'signal.original_event.category') === 'anomoly'
);
expect(buildingBlock).not.eql(undefined);
const signal = buildingBlock!._source.signal;
expect(signal).eql({
rule: signal.rule,
group: signal.group,
original_time: signal.original_time,
status: 'open',
depth: 1,
ancestors: [
{
depth: 0,
id: 'VhXOBmkBR346wHgnLP8T',
index: 'auditbeat-8.0.0-2019.02.19-000001',
type: 'event',
},
],
original_event: {
action: 'changed-promiscuous-mode-on-device',
category: 'anomoly',
module: 'auditd',
},
parent: {
depth: 0,
id: 'VhXOBmkBR346wHgnLP8T',
index: 'auditbeat-8.0.0-2019.02.19-000001',
type: 'event',
},
parents: [
{
depth: 0,
id: 'VhXOBmkBR346wHgnLP8T',
index: 'auditbeat-8.0.0-2019.02.19-000001',
type: 'event',
},
],
_meta: {
version: SIGNALS_TEMPLATE_VERSION,
},
});
});
it('generates shell signals from EQL sequences in the expected form', async () => {
const rule: EqlCreateSchema = {
...getRuleForSignalTesting(['auditbeat-*']),
rule_id: 'eql-rule',
type: 'eql',
language: 'eql',
query: 'sequence by host.name [anomoly where true] [any where true]',
};
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 3, [id]);
const signalsOpen = await getSignalsByRuleIds(supertest, ['eql-rule']);
const sequenceSignal = signalsOpen.hits.hits.find(
(signal) => signal._source.signal.depth === 2
);
const signal = sequenceSignal!._source.signal;
const eventIds = signal.parents.map((event) => event.id);
expect(signal).eql({
status: 'open',
depth: 2,
@ -295,7 +562,7 @@ export default ({ getService }: FtrProviderContext) => {
ancestors: [
{
depth: 0,
id: 'gCF0B2kBR346wHgnb7m0',
id: 'VhXOBmkBR346wHgnLP8T',
index: 'auditbeat-8.0.0-2019.02.19-000001',
type: 'event',
},
@ -308,7 +575,7 @@ export default ({ getService }: FtrProviderContext) => {
},
{
depth: 0,
id: 'CCF0B2kBR346wHgngLtX',
id: '4hbXBmkBR346wHgn6fdp',
index: 'auditbeat-8.0.0-2019.02.19-000001',
type: 'event',
},
@ -341,6 +608,254 @@ export default ({ getService }: FtrProviderContext) => {
},
});
});
it('generates up to max_signals with an EQL rule', async () => {
const rule: EqlCreateSchema = {
...getRuleForSignalTesting(['auditbeat-*']),
rule_id: 'eql-rule',
type: 'eql',
language: 'eql',
query: 'sequence by host.name [any where true] [any where true]',
};
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
// For EQL rules, max_signals is the maximum number of detected sequences: each sequence has a building block
// alert for each event in the sequence, so max_signals=100 results in 200 building blocks in addition to
// 100 regular alerts
await waitForSignalsToBePresent(supertest, 300, [id]);
const signalsOpen = await getSignalsByIds(supertest, [id], 1000);
expect(signalsOpen.hits.hits.length).eql(300);
const shellSignals = signalsOpen.hits.hits.filter(
(signal) => signal._source.signal.depth === 2
);
const buildingBlocks = signalsOpen.hits.hits.filter(
(signal) => signal._source.signal.depth === 1
);
expect(shellSignals.length).eql(100);
expect(buildingBlocks.length).eql(200);
});
});
describe('Threshold Rules', () => {
it('generates 1 signal from Threshold rules when threshold is met', async () => {
const ruleId = 'threshold-rule';
const rule: ThresholdCreateSchema = {
...getRuleForSignalTesting(['auditbeat-*']),
rule_id: ruleId,
type: 'threshold',
language: 'kuery',
query: '*:*',
threshold: {
field: 'host.id',
value: 700,
},
};
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 1, [id]);
const signalsOpen = await getSignalsByRuleIds(supertest, [ruleId]);
expect(signalsOpen.hits.hits.length).eql(1);
const signal = signalsOpen.hits.hits[0];
expect(signal._source.signal.threshold_result).eql({
terms: [
{
field: 'host.id',
value: '8cc95778cce5407c809480e8e32ad76b',
},
],
count: 788,
from: '1900-01-01T00:00:00.000Z',
});
});
it('generates 2 signals from Threshold rules when threshold is met', async () => {
const ruleId = 'threshold-rule';
const rule: ThresholdCreateSchema = {
...getRuleForSignalTesting(['auditbeat-*']),
rule_id: ruleId,
type: 'threshold',
language: 'kuery',
query: '*:*',
threshold: {
field: 'host.id',
value: 100,
},
};
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 2, [id]);
const signalsOpen = await getSignalsByRuleIds(supertest, [ruleId]);
expect(signalsOpen.hits.hits.length).eql(2);
});
it('applies the provided query before bucketing ', async () => {
const ruleId = 'threshold-rule';
const rule: ThresholdCreateSchema = {
...getRuleForSignalTesting(['auditbeat-*']),
rule_id: ruleId,
type: 'threshold',
language: 'kuery',
query: 'host.id:"2ab45fc1c41e4c84bbd02202a7e5761f"',
threshold: {
field: 'process.name',
value: 21,
},
};
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 1, [id]);
const signalsOpen = await getSignalsByRuleIds(supertest, [ruleId]);
expect(signalsOpen.hits.hits.length).eql(1);
});
it('generates no signals from Threshold rules when threshold is met and cardinality is not met', async () => {
const ruleId = 'threshold-rule';
const rule: ThresholdCreateSchema = {
...getRuleForSignalTesting(['auditbeat-*']),
rule_id: ruleId,
type: 'threshold',
language: 'kuery',
query: '*:*',
threshold: {
field: 'host.id',
value: 100,
cardinality: [
{
field: 'destination.ip',
value: 100,
},
],
},
};
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
const signalsOpen = await getSignalsByRuleIds(supertest, [ruleId]);
expect(signalsOpen.hits.hits.length).eql(0);
});
it('generates no signals from Threshold rules when cardinality is met and threshold is not met', async () => {
const ruleId = 'threshold-rule';
const rule: ThresholdCreateSchema = {
...getRuleForSignalTesting(['auditbeat-*']),
rule_id: ruleId,
type: 'threshold',
language: 'kuery',
query: '*:*',
threshold: {
field: 'host.id',
value: 1000,
cardinality: [
{
field: 'destination.ip',
value: 5,
},
],
},
};
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
const signalsOpen = await getSignalsByRuleIds(supertest, [ruleId]);
expect(signalsOpen.hits.hits.length).eql(0);
});
it('generates signals from Threshold rules when threshold and cardinality are both met', async () => {
const ruleId = 'threshold-rule';
const rule: ThresholdCreateSchema = {
...getRuleForSignalTesting(['auditbeat-*']),
rule_id: ruleId,
type: 'threshold',
language: 'kuery',
query: '*:*',
threshold: {
field: 'host.id',
value: 100,
cardinality: [
{
field: 'destination.ip',
value: 5,
},
],
},
};
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
const signalsOpen = await getSignalsByRuleIds(supertest, [ruleId]);
expect(signalsOpen.hits.hits.length).eql(1);
const signal = signalsOpen.hits.hits[0];
expect(signal._source.signal.threshold_result).eql({
terms: [
{
field: 'host.id',
value: '8cc95778cce5407c809480e8e32ad76b',
},
],
cardinality: [
{
field: 'destination.ip',
value: 7,
},
],
count: 788,
from: '1900-01-01T00:00:00.000Z',
});
});
it('should not generate signals if only one field meets the threshold requirement', async () => {
const ruleId = 'threshold-rule';
const rule: ThresholdCreateSchema = {
...getRuleForSignalTesting(['auditbeat-*']),
rule_id: ruleId,
type: 'threshold',
language: 'kuery',
query: '*:*',
threshold: {
field: ['host.id', 'process.name'],
value: 22,
},
};
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
const signalsOpen = await getSignalsByRuleIds(supertest, [ruleId]);
expect(signalsOpen.hits.hits.length).eql(0);
});
it('generates signals from Threshold rules when bucketing by multiple fields', async () => {
const ruleId = 'threshold-rule';
const rule: ThresholdCreateSchema = {
...getRuleForSignalTesting(['auditbeat-*']),
rule_id: ruleId,
type: 'threshold',
language: 'kuery',
query: '*:*',
threshold: {
field: ['host.id', 'process.name', 'event.module'],
value: 21,
},
};
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
const signalsOpen = await getSignalsByRuleIds(supertest, [ruleId]);
expect(signalsOpen.hits.hits.length).eql(1);
const signal = signalsOpen.hits.hits[0];
expect(signal._source.signal.threshold_result).eql({
terms: [
{
field: 'event.module',
value: 'system',
},
{
field: 'host.id',
value: '2ab45fc1c41e4c84bbd02202a7e5761f',
},
{
field: 'process.name',
value: 'sshd',
},
],
count: 21,
from: '1900-01-01T00:00:00.000Z',
});
});
});
});

View file

@ -778,6 +778,17 @@ export const countDownES = async (
);
};
/**
* 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 refreshIndex = async (es: KibanaClient, index?: string) => {
await es.indices.refresh({
index,
});
};
/**
* Does a plain countdown and checks against a boolean to determine if to wait and try again.
* This is useful for over the wire things that can cause issues such as conflict or timeouts
@ -1107,7 +1118,7 @@ export const installPrePackagedRules = async (
*/
export const createRuleWithExceptionEntries = async (
supertest: SuperTest<supertestAsPromised.Test>,
rule: QueryCreateSchema,
rule: CreateRulesSchema,
entries: NonEmptyEntriesArray[]
): Promise<FullResponseSchema> => {
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -1141,7 +1152,7 @@ export const createRuleWithExceptionEntries = async (
// the rule to sometimes not filter correctly the first time with an exception list
// or other timing issues. Then afterwards wait for the rule to have succeeded before
// returning.
const ruleWithException: QueryCreateSchema = {
const ruleWithException: CreateRulesSchema = {
...rule,
enabled: false,
exceptions_list: [
@ -1202,3 +1213,16 @@ export const deleteMigrations = async ({
)
);
};
export const getOpenSignals = async (
supertest: SuperTest<supertestAsPromised.Test>,
es: KibanaClient,
rule: FullResponseSchema
) => {
await waitForRuleSuccessOrStatus(supertest, rule.id);
// Critically important that we wait for rule success AND refresh the write index in that order before we
// assert that no signals were created. Otherwise, signals could be written but not available to query yet
// when we search, causing tests that check that signals are NOT created to pass when they should fail.
await refreshIndex(es, rule.output_index);
return getSignalsByIds(supertest, [rule.id]);
};