Add ML rule API integration tests and test for removing action (#98100)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marshall Main 2021-05-04 17:42:41 -04:00 committed by GitHub
parent ebc5b90123
commit 29e48b8655
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 1457 additions and 6 deletions

View file

@ -68,8 +68,8 @@ export const updateRulesRoute = (router: SecuritySolutionPluginRouter, ml: Setup
alertsClient,
savedObjectsClient,
enabled: request.body.enabled ?? true,
actions: request.body.actions,
throttle: request.body.throttle,
actions: request.body.actions ?? [],
throttle: request.body.throttle ?? 'no_actions',
name: request.body.name,
});
const ruleStatuses = await ruleStatusClient.find({

View file

@ -33,7 +33,6 @@ export const updateRules = async ({
}
const typeSpecificParams = typeSpecificSnakeToCamel(ruleUpdate);
const throttle = ruleUpdate.throttle ?? null;
const enabled = ruleUpdate.enabled ?? true;
const newInternalRule: InternalRuleUpdate = {
name: ruleUpdate.name,
@ -73,7 +72,10 @@ export const updateRules = async ({
...typeSpecificParams,
},
schedule: { interval: ruleUpdate.interval ?? '5m' },
actions: throttle === 'rule' ? (ruleUpdate.actions ?? []).map(transformRuleToAlertAction) : [],
actions:
ruleUpdate.throttle === 'rule'
? (ruleUpdate.actions ?? []).map(transformRuleToAlertAction)
: [],
throttle: null,
notifyWhen: null,
};

View file

@ -0,0 +1,256 @@
/*
* 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 { MachineLearningCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
createRule,
createRuleWithExceptionEntries,
createSignalsIndex,
deleteAllAlerts,
deleteSignalsIndex,
getOpenSignals,
} from '../../utils';
import {
createListsIndex,
deleteAllExceptions,
deleteListsIndex,
importFile,
} from '../../../lists_api_integration/utils';
// 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 siemModule = 'siem_auditbeat';
const mlJobId = 'linux_anomalous_network_activity_ecs';
const testRule: MachineLearningCreateSchema = {
name: 'Test ML rule',
description: 'Test ML rule description',
risk_score: 50,
severity: 'critical',
type: 'machine_learning',
anomaly_threshold: 30,
machine_learning_job_id: mlJobId,
from: '1900-01-01T00:00:00.000Z',
};
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;
}
describe('Generating signals from ml anomalies', () => {
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('auditbeat/hosts');
await executeSetupModuleRequest(siemModule, 200);
await forceStartDatafeeds(mlJobId, 200);
await esArchiver.load('security_solution/anomalies');
});
after(async () => {
await esArchiver.unload('auditbeat/hosts');
await esArchiver.unload('security_solution/anomalies');
});
beforeEach(async () => {
await createSignalsIndex(supertest);
});
afterEach(async () => {
await deleteSignalsIndex(supertest);
await deleteAllAlerts(supertest);
});
it('should create 1 alert from ML rule when record meets anomaly_threshold', async () => {
const createdRule = await createRule(supertest, testRule);
const signalsOpen = await getOpenSignals(supertest, es, createdRule);
expect(signalsOpen.hits.hits.length).eql(1);
const signal = signalsOpen.hits.hits[0];
expect(signal._source).eql({
'@timestamp': signal._source['@timestamp'],
actual: [1],
bucket_span: 900,
by_field_name: 'process.name',
by_field_value: 'store',
detector_index: 0,
function: 'rare',
function_description: 'rare',
influencers: [
{ influencer_field_name: 'user.name', influencer_field_values: ['root'] },
{ influencer_field_name: 'process.name', influencer_field_values: ['store'] },
{ influencer_field_name: 'host.name', influencer_field_values: ['mothra'] },
],
initial_record_score: 33.36147565024334,
is_interim: false,
job_id: 'linux_anomalous_network_activity_ecs',
multi_bucket_impact: 0,
probability: 0.007820139656036713,
record_score: 33.36147565024334,
result_type: 'record',
timestamp: 1605567488000,
typical: [0.007820139656036711],
user: { name: ['root'] },
process: { name: ['store'] },
host: { name: ['mothra'] },
event: { kind: 'signal' },
signal: {
_meta: { version: 35 },
parents: [
{
id:
'linux_anomalous_network_activity_ecs_record_1586274300000_900_0_-96106189301704594950079884115725560577_5',
type: 'event',
index: '.ml-anomalies-custom-linux_anomalous_network_activity_ecs',
depth: 0,
},
],
ancestors: [
{
id:
'linux_anomalous_network_activity_ecs_record_1586274300000_900_0_-96106189301704594950079884115725560577_5',
type: 'event',
index: '.ml-anomalies-custom-linux_anomalous_network_activity_ecs',
depth: 0,
},
],
status: 'open',
rule: {
id: createdRule.id,
rule_id: createdRule.rule_id,
created_at: createdRule.created_at,
updated_at: signal._source.signal.rule.updated_at,
actions: [],
interval: '5m',
name: 'Test ML rule',
tags: [],
enabled: true,
created_by: 'elastic',
updated_by: 'elastic',
throttle: null,
description: 'Test ML rule description',
risk_score: 50,
severity: 'critical',
output_index: '.siem-signals-default',
author: [],
false_positives: [],
from: '1900-01-01T00:00:00.000Z',
max_signals: 100,
risk_score_mapping: [],
severity_mapping: [],
threat: [],
to: 'now',
references: [],
version: 1,
exceptions_list: [],
immutable: false,
type: 'machine_learning',
anomaly_threshold: 30,
machine_learning_job_id: ['linux_anomalous_network_activity_ecs'],
},
depth: 1,
parent: {
id:
'linux_anomalous_network_activity_ecs_record_1586274300000_900_0_-96106189301704594950079884115725560577_5',
type: 'event',
index: '.ml-anomalies-custom-linux_anomalous_network_activity_ecs',
depth: 0,
},
original_time: '2020-11-16T22:58:08.000Z',
},
});
});
it('should create 7 alerts from ML rule when records meet anomaly_threshold', async () => {
const rule: MachineLearningCreateSchema = {
...testRule,
anomaly_threshold: 20,
};
const createdRule = await createRule(supertest, rule);
const signalsOpen = await getOpenSignals(supertest, es, createdRule);
expect(signalsOpen.hits.hits.length).eql(7);
});
describe('with non-value list exception', () => {
afterEach(async () => {
await deleteAllExceptions(es);
});
it('generates no signals when an exception is added for an ML rule', async () => {
const createdRule = await createRuleWithExceptionEntries(supertest, testRule, [
[
{
field: 'host.name',
operator: 'included',
type: 'match',
value: 'mothra',
},
],
]);
const signalsOpen = await getOpenSignals(supertest, es, createdRule);
expect(signalsOpen.hits.hits.length).equal(0);
});
});
describe('with value list exception', () => {
beforeEach(async () => {
await createListsIndex(supertest);
});
afterEach(async () => {
await deleteListsIndex(supertest);
await deleteAllExceptions(es);
});
it('generates no signals when a value list exception is added for an ML rule', async () => {
const valueListId = 'value-list-id';
await importFile(supertest, 'keyword', ['mothra'], valueListId);
const createdRule = await createRuleWithExceptionEntries(supertest, testRule, [
[
{
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

@ -19,6 +19,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
loadTestFile(require.resolve('./create_rules'));
loadTestFile(require.resolve('./create_rules_bulk'));
loadTestFile(require.resolve('./create_index'));
loadTestFile(require.resolve('./create_ml'));
loadTestFile(require.resolve('./create_threat_matching'));
loadTestFile(require.resolve('./create_exceptions'));
loadTestFile(require.resolve('./delete_rules'));

View file

@ -26,6 +26,7 @@ import {
createNewAction,
findImmutableRuleById,
getPrePackagedRulesStatus,
getSimpleRuleOutput,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
@ -61,6 +62,21 @@ export default ({ getService }: FtrProviderContext) => {
expect(bodyToCompare).to.eql(expected);
});
it('should be able to add a new webhook action and then remove the action from the rule again', async () => {
const hookAction = await createNewAction(supertest);
const rule = getSimpleRule();
await createRule(supertest, rule);
const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, false, rule);
await updateRule(supertest, ruleToUpdate);
const ruleAfterActionRemoved = await updateRule(supertest, rule);
const bodyToCompare = removeServerGeneratedProperties(ruleAfterActionRemoved);
const expected = {
...getSimpleRuleOutput(),
version: 3, // version bump is required since this is an updated rule and this is part of the testing that we do bump the version number on update
};
expect(bodyToCompare).to.eql(expected);
});
it('should be able to create a new webhook action and attach it to a rule without a meta field and run it correctly', async () => {
const hookAction = await createNewAction(supertest);
const rule = getSimpleRule();

View file

@ -683,7 +683,7 @@ export const getWebHookAction = () => ({
export const getRuleWithWebHookAction = (
id: string,
enabled = false,
rule?: QueryCreateSchema
rule?: CreateRulesSchema
): CreateRulesSchema | UpdateRulesSchema => {
const finalRule = rule != null ? { ...rule, enabled } : getSimpleRule('rule-1', enabled);
return {
@ -888,7 +888,7 @@ export const createNewAction = async (supertest: SuperTest<supertestAsPromised.T
/**
* Helper to cut down on the noise in some of the tests. This
* creates a new action and expects a 200 and does not do any retries.
* uses the find API to get an immutable rule by id.
* @param supertest The supertest deps
*/
export const findImmutableRuleById = async (

File diff suppressed because it is too large Load diff