Skip to content

Commit a779eca

Browse files
rylndelasticmachine
andcommitted
[SIEM] Add license checks for ML Rules on the backend (#61023)
* WIP: Check license on simple rule creation We'll add this to the rest of the routes momentarily. * Add license checks around all rule-modifying endpoints This ensures that you cannot create nor update an ML Rule if your license is not Platinum (or Trial). Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 83cbb6e commit a779eca

22 files changed

Lines changed: 308 additions & 14 deletions

x-pack/legacy/plugins/siem/common/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,9 @@ export const DETECTION_ENGINE_QUERY_SIGNALS_URL = `${DETECTION_ENGINE_SIGNALS_UR
9494
*/
9595
export const UNAUTHENTICATED_USER = 'Unauthenticated';
9696

97+
/*
98+
Licensing requirements
99+
*/
100+
export const MINIMUM_ML_LICENSE = 'platinum';
101+
97102
export const NOTIFICATION_THROTTLE_RULE = 'rule';

x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ import {
1212
} from '../../../../../../../../../src/core/server/mocks';
1313
import { alertsClientMock } from '../../../../../../../../plugins/alerting/server/mocks';
1414
import { actionsClientMock } from '../../../../../../../../plugins/actions/server/mocks';
15+
import { licensingMock } from '../../../../../../../../plugins/licensing/server/mocks';
1516

1617
const createMockClients = () => ({
1718
actionsClient: actionsClientMock.create(),
1819
alertsClient: alertsClientMock.create(),
1920
clusterClient: elasticsearchServiceMock.createScopedClusterClient(),
21+
licensing: { license: licensingMock.createLicenseMock() },
2022
savedObjectsClient: savedObjectsClientMock.create(),
2123
siemClient: { signalsIndex: 'mockSignalsIndex' },
2224
});
@@ -33,6 +35,7 @@ const createRequestContextMock = (
3335
elasticsearch: { ...coreContext.elasticsearch, dataClient: clients.clusterClient },
3436
savedObjects: { client: clients.savedObjectsClient },
3537
},
38+
licensing: clients.licensing,
3639
siem: { getSiemClient: jest.fn(() => clients.siemClient) },
3740
} as unknown) as RequestHandlerContext;
3841
};

x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -295,18 +295,30 @@ export const getCreateRequest = () =>
295295
body: typicalPayload(),
296296
});
297297

298-
export const createMlRuleRequest = () => {
298+
export const typicalMlRulePayload = () => {
299299
const { query, language, index, ...mlParams } = typicalPayload();
300300

301+
return {
302+
...mlParams,
303+
type: 'machine_learning',
304+
anomaly_threshold: 58,
305+
machine_learning_job_id: 'typical-ml-job-id',
306+
};
307+
};
308+
309+
export const createMlRuleRequest = () => {
301310
return requestMock.create({
302311
method: 'post',
303312
path: DETECTION_ENGINE_RULES_URL,
304-
body: {
305-
...mlParams,
306-
type: 'machine_learning',
307-
anomaly_threshold: 50,
308-
machine_learning_job_id: 'some-uuid',
309-
},
313+
body: typicalMlRulePayload(),
314+
});
315+
};
316+
317+
export const createBulkMlRuleRequest = () => {
318+
return requestMock.create({
319+
method: 'post',
320+
path: DETECTION_ENGINE_RULES_URL,
321+
body: [typicalMlRulePayload()],
310322
});
311323
};
312324

x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ export const getSimpleRule = (ruleId = 'rule-1'): Partial<OutputRuleAlertRest> =
2323
query: 'user.name: root or user.name: admin',
2424
});
2525

26+
/**
27+
* This is a typical ML rule for testing
28+
* @param ruleId
29+
*/
30+
export const getSimpleMlRule = (ruleId = 'rule-1'): Partial<OutputRuleAlertRest> => ({
31+
name: 'Simple Rule Query',
32+
description: 'Simple Rule Query',
33+
risk_score: 1,
34+
rule_id: ruleId,
35+
severity: 'high',
36+
type: 'machine_learning',
37+
anomaly_threshold: 44,
38+
machine_learning_job_id: 'some_job_id',
39+
});
40+
2641
/**
2742
* This is a typical simple rule for testing that is easy for most basic testing
2843
* @param ruleId

x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getFindResultWithSingleHit,
1414
getEmptyFindResult,
1515
getResult,
16+
createBulkMlRuleRequest,
1617
} from '../__mocks__/request_responses';
1718
import { requestContextMock, serverMock, requestMock } from '../__mocks__';
1819
import { createRulesBulkRoute } from './create_rules_bulk_route';
@@ -56,6 +57,22 @@ describe('create_rules_bulk', () => {
5657
});
5758

5859
describe('unhappy paths', () => {
60+
it('returns an error object if creating an ML rule with an insufficient license', async () => {
61+
(context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false);
62+
63+
const response = await server.inject(createBulkMlRuleRequest(), context);
64+
expect(response.status).toEqual(200);
65+
expect(response.body).toEqual([
66+
{
67+
error: {
68+
message: 'Your license does not support machine learning. Please upgrade your license.',
69+
status_code: 400,
70+
},
71+
rule_id: 'rule-1',
72+
},
73+
]);
74+
});
75+
5976
it('returns an error object if the index does not exist', async () => {
6077
clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex());
6178
const response = await server.inject(getReadBulkRequest(), context);

x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
createBulkErrorObject,
2020
buildRouteValidation,
2121
buildSiemResponse,
22+
validateLicenseForRuleType,
2223
} from '../utils';
2324
import { createRulesBulkSchema } from '../schemas/create_rules_bulk_schema';
2425
import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema';
@@ -90,6 +91,8 @@ export const createRulesBulkRoute = (router: IRouter) => {
9091
} = payloadRule;
9192
const ruleIdOrUuid = ruleId ?? uuid.v4();
9293
try {
94+
validateLicenseForRuleType({ license: context.licensing.license, ruleType: type });
95+
9396
const finalIndex = outputIndex ?? siemClient.signalsIndex;
9497
const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex);
9598
if (!indexExists) {

x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,31 @@ describe('create_rules', () => {
5959
expect(response.status).toEqual(404);
6060
expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
6161
});
62+
63+
it('returns 200 if license is not platinum', async () => {
64+
(context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false);
65+
66+
const response = await server.inject(getCreateRequest(), context);
67+
expect(response.status).toEqual(200);
68+
});
6269
});
6370

6471
describe('creating an ML Rule', () => {
6572
it('is successful', async () => {
6673
const response = await server.inject(createMlRuleRequest(), context);
6774
expect(response.status).toEqual(200);
6875
});
76+
77+
it('rejects the request if licensing is not platinum', async () => {
78+
(context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false);
79+
80+
const response = await server.inject(createMlRuleRequest(), context);
81+
expect(response.status).toEqual(400);
82+
expect(response.body).toEqual({
83+
message: 'Your license does not support machine learning. Please upgrade your license.',
84+
status_code: 400,
85+
});
86+
});
6987
});
7088

7189
describe('creating a Notification if throttle and actions were provided ', () => {

x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
1616
import { transformValidate } from './validate';
1717
import { getIndexExists } from '../../index/get_index_exists';
1818
import { createRulesSchema } from '../schemas/create_rules_schema';
19-
import { buildRouteValidation, transformError, buildSiemResponse } from '../utils';
19+
import {
20+
buildRouteValidation,
21+
transformError,
22+
buildSiemResponse,
23+
validateLicenseForRuleType,
24+
} from '../utils';
2025
import { createNotifications } from '../../notifications/create_notifications';
2126

2227
export const createRulesRoute = (router: IRouter): void => {
@@ -66,6 +71,7 @@ export const createRulesRoute = (router: IRouter): void => {
6671
const siemResponse = buildSiemResponse(response);
6772

6873
try {
74+
validateLicenseForRuleType({ license: context.licensing.license, ruleType: type });
6975
if (!context.alerting || !context.actions) {
7076
return siemResponse.error({ statusCode: 404 });
7177
}

x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
ruleIdsToNdJsonString,
1010
rulesToNdJsonString,
1111
getSimpleRuleWithId,
12+
getSimpleRule,
13+
getSimpleMlRule,
1214
} from '../__mocks__/utils';
1315
import {
1416
getImportRulesRequest,
@@ -102,6 +104,30 @@ describe('import_rules_route', () => {
102104
});
103105

104106
describe('unhappy paths', () => {
107+
it('returns an error object if creating an ML rule with an insufficient license', async () => {
108+
(context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false);
109+
const rules = [getSimpleRule(), getSimpleMlRule('rule-2')];
110+
const hapiStreamWithMlRule = buildHapiStream(rulesToNdJsonString(rules));
111+
request = getImportRulesRequest(hapiStreamWithMlRule);
112+
113+
const response = await server.inject(request, context);
114+
expect(response.status).toEqual(200);
115+
expect(response.body).toEqual({
116+
errors: [
117+
{
118+
error: {
119+
message:
120+
'Your license does not support machine learning. Please upgrade your license.',
121+
status_code: 400,
122+
},
123+
rule_id: 'rule-2',
124+
},
125+
],
126+
success: false,
127+
success_count: 1,
128+
});
129+
});
130+
105131
test('returns error if createPromiseFromStreams throws error', async () => {
106132
jest
107133
.spyOn(createRulesStreamFromNdJson, 'createRulesStreamFromNdJson')

x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
isImportRegular,
2525
transformError,
2626
buildSiemResponse,
27+
validateLicenseForRuleType,
2728
} from '../utils';
2829
import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson';
2930
import { ImportRuleAlertRest } from '../../types';
@@ -146,6 +147,11 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config
146147
} = parsedRule;
147148

148149
try {
150+
validateLicenseForRuleType({
151+
license: context.licensing.license,
152+
ruleType: type,
153+
});
154+
149155
const signalsIndex = siemClient.signalsIndex;
150156
const indexExists = await getIndexExists(
151157
clusterClient.callAsCurrentUser,

0 commit comments

Comments
 (0)