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:
parent
ebc5b90123
commit
29e48b8655
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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'));
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 (
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue