[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:
parent
7927923f0e
commit
a90afbf1ec
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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]);
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue