Skip to content

Commit

Permalink
Add license checks around all rule-modifying endpoints
Browse files Browse the repository at this point in the history
This ensures that you cannot create nor update an ML Rule if your
license is not Platinum (or Trial).
  • Loading branch information
rylnd committed Mar 24, 2020
1 parent 67eb033 commit dd318c1
Show file tree
Hide file tree
Showing 14 changed files with 200 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -294,18 +294,30 @@ export const getCreateRequest = () =>
body: typicalPayload(),
});

export const createMlRuleRequest = () => {
export const typicalMlRulePayload = () => {
const { query, language, index, ...mlParams } = typicalPayload();

return {
...mlParams,
type: 'machine_learning',
anomaly_threshold: 58,
machine_learning_job_id: 'typical-ml-job-id',
};
};

export const createMlRuleRequest = () => {
return requestMock.create({
method: 'post',
path: DETECTION_ENGINE_RULES_URL,
body: {
...mlParams,
type: 'machine_learning',
anomaly_threshold: 50,
machine_learning_job_id: 'some-uuid',
},
body: typicalMlRulePayload(),
});
};

export const createBulkMlRuleRequest = () => {
return requestMock.create({
method: 'post',
path: DETECTION_ENGINE_RULES_URL,
body: [typicalMlRulePayload()],
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ export const getSimpleRule = (ruleId = 'rule-1'): Partial<OutputRuleAlertRest> =
query: 'user.name: root or user.name: admin',
});

/**
* This is a typical ML rule for testing
* @param ruleId
*/
export const getSimpleMlRule = (ruleId = 'rule-1'): Partial<OutputRuleAlertRest> => ({
name: 'Simple Rule Query',
description: 'Simple Rule Query',
risk_score: 1,
rule_id: ruleId,
severity: 'high',
type: 'machine_learning',
anomaly_threshold: 44,
machine_learning_job_id: 'some_job_id',
});

/**
* This is a typical simple rule for testing that is easy for most basic testing
* @param ruleId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getFindResultWithSingleHit,
getEmptyFindResult,
getResult,
createBulkMlRuleRequest,
} from '../__mocks__/request_responses';
import { requestContextMock, serverMock, requestMock } from '../__mocks__';
import { createRulesBulkRoute } from './create_rules_bulk_route';
Expand Down Expand Up @@ -56,6 +57,22 @@ describe('create_rules_bulk', () => {
});

describe('unhappy paths', () => {
it('returns an error object if creating an ML rule with an insufficient license', async () => {
(context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false);

const response = await server.inject(createBulkMlRuleRequest(), context);
expect(response.status).toEqual(200);
expect(response.body).toEqual([
{
error: {
message: 'Your license does not support machine learning. Please upgrade your license.',
status_code: 400,
},
rule_id: 'rule-1',
},
]);
});

it('returns an error object if the index does not exist', async () => {
clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex());
const response = await server.inject(getReadBulkRequest(), context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
createBulkErrorObject,
buildRouteValidation,
buildSiemResponse,
validateLicenseForRuleType,
} from '../utils';
import { createRulesBulkSchema } from '../schemas/create_rules_bulk_schema';
import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema';
Expand Down Expand Up @@ -90,6 +91,8 @@ export const createRulesBulkRoute = (router: IRouter) => {
} = payloadRule;
const ruleIdOrUuid = ruleId ?? uuid.v4();
try {
validateLicenseForRuleType({ license: context.licensing.license, ruleType: type });

const finalIndex = outputIndex ?? siemClient.signalsIndex;
const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex);
if (!indexExists) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
ruleIdsToNdJsonString,
rulesToNdJsonString,
getSimpleRuleWithId,
getSimpleRule,
getSimpleMlRule,
} from '../__mocks__/utils';
import {
getImportRulesRequest,
Expand Down Expand Up @@ -102,6 +104,30 @@ describe('import_rules_route', () => {
});

describe('unhappy paths', () => {
it('returns an error object if creating an ML rule with an insufficient license', async () => {
(context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false);
const rules = [getSimpleRule(), getSimpleMlRule('rule-2')];
const hapiStreamWithMlRule = buildHapiStream(rulesToNdJsonString(rules));
request = getImportRulesRequest(hapiStreamWithMlRule);

const response = await server.inject(request, context);
expect(response.status).toEqual(200);
expect(response.body).toEqual({
errors: [
{
error: {
message:
'Your license does not support machine learning. Please upgrade your license.',
status_code: 400,
},
rule_id: 'rule-2',
},
],
success: false,
success_count: 1,
});
});

test('returns error if createPromiseFromStreams throws error', async () => {
jest
.spyOn(createRulesStreamFromNdJson, 'createRulesStreamFromNdJson')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
isImportRegular,
transformError,
buildSiemResponse,
validateLicenseForRuleType,
} from '../utils';
import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson';
import { ImportRuleAlertRest } from '../../types';
Expand Down Expand Up @@ -146,6 +147,11 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config
} = parsedRule;

try {
validateLicenseForRuleType({
license: context.licensing.license,
ruleType: type,
});

const signalsIndex = siemClient.signalsIndex;
const indexExists = await getIndexExists(
clusterClient.callAsCurrentUser,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getFindResultWithSingleHit,
getPatchBulkRequest,
getResult,
typicalMlRulePayload,
} from '../__mocks__/request_responses';
import { serverMock, requestContextMock, requestMock } from '../__mocks__';
import { patchRulesBulkRoute } from './patch_rules_bulk_route';
Expand Down Expand Up @@ -88,6 +89,27 @@ describe('patch_rules_bulk', () => {
expect(response.status).toEqual(404);
expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
});

it('rejects patching of an ML rule with an insufficient license', async () => {
(context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false);
const request = requestMock.create({
method: 'patch',
path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
body: [typicalMlRulePayload()],
});

const response = await server.inject(request, context);
expect(response.status).toEqual(200);
expect(response.body).toEqual([
{
error: {
message: 'Your license does not support machine learning. Please upgrade your license.',
status_code: 400,
},
rule_id: 'rule-1',
},
]);
});
});

describe('request validation', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import {
IRuleSavedAttributesSavedObjectAttributes,
PatchRuleAlertParamsRest,
} from '../../rules/types';
import { transformBulkError, buildRouteValidation, buildSiemResponse } from '../utils';
import {
transformBulkError,
buildRouteValidation,
buildSiemResponse,
validateLicenseForRuleType,
} from '../utils';
import { getIdBulkError } from './utils';
import { transformValidateBulkError, validate } from './validate';
import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema';
Expand Down Expand Up @@ -80,6 +85,10 @@ export const patchRulesBulkRoute = (router: IRouter) => {
} = payloadRule;
const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)';
try {
if (type) {
validateLicenseForRuleType({ license: context.licensing.license, ruleType: type });
}

const rule = await patchRules({
alertsClient,
actionsClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
typicalPayload,
getFindResultWithSingleHit,
nonRuleFindResult,
typicalMlRulePayload,
} from '../__mocks__/request_responses';
import { requestContextMock, serverMock, requestMock } from '../__mocks__';
import { patchRulesRoute } from './patch_rules_route';
Expand Down Expand Up @@ -109,6 +110,22 @@ describe('patch_rules', () => {
})
);
});

it('rejects patching a rule to ML if licensing is not platinum', async () => {
(context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false);
const request = requestMock.create({
method: 'patch',
path: DETECTION_ENGINE_RULES_URL,
body: typicalMlRulePayload(),
});
const response = await server.inject(request, context);

expect(response.status).toEqual(400);
expect(response.body).toEqual({
message: 'Your license does not support machine learning. Please upgrade your license.',
status_code: 400,
});
});
});

describe('request validation', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import {
IRuleSavedAttributesSavedObjectAttributes,
} from '../../rules/types';
import { patchRulesSchema } from '../schemas/patch_rules_schema';
import { buildRouteValidation, transformError, buildSiemResponse } from '../utils';
import {
buildRouteValidation,
transformError,
buildSiemResponse,
validateLicenseForRuleType,
} from '../utils';
import { getIdError } from './utils';
import { transformValidate } from './validate';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
Expand Down Expand Up @@ -65,6 +70,10 @@ export const patchRulesRoute = (router: IRouter) => {
const siemResponse = buildSiemResponse(response);

try {
if (type) {
validateLicenseForRuleType({ license: context.licensing.license, ruleType: type });
}

if (!context.alerting || !context.actions) {
return siemResponse.error({ statusCode: 404 });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getFindResultWithSingleHit,
getUpdateBulkRequest,
getFindResultStatus,
typicalMlRulePayload,
} from '../__mocks__/request_responses';
import { serverMock, requestContextMock, requestMock } from '../__mocks__';
import { updateRulesBulkRoute } from './update_rules_bulk_route';
Expand Down Expand Up @@ -83,6 +84,27 @@ describe('update_rules_bulk', () => {
expect(response.status).toEqual(200);
expect(response.body).toEqual(expected);
});

it('returns an error object if creating an ML rule with an insufficient license', async () => {
(context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false);
const request = requestMock.create({
method: 'put',
path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`,
body: [typicalMlRulePayload()],
});

const response = await server.inject(request, context);
expect(response.status).toEqual(200);
expect(response.body).toEqual([
{
error: {
message: 'Your license does not support machine learning. Please upgrade your license.',
status_code: 400,
},
rule_id: 'rule-1',
},
]);
});
});

describe('request validation', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import {
} from '../../rules/types';
import { getIdBulkError } from './utils';
import { transformValidateBulkError, validate } from './validate';
import { buildRouteValidation, transformBulkError, buildSiemResponse } from '../utils';
import {
buildRouteValidation,
transformBulkError,
buildSiemResponse,
validateLicenseForRuleType,
} from '../utils';
import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
import { updateRules } from '../../rules/update_rules';
Expand Down Expand Up @@ -83,6 +88,8 @@ export const updateRulesBulkRoute = (router: IRouter) => {
const finalIndex = outputIndex ?? siemClient.signalsIndex;
const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)';
try {
validateLicenseForRuleType({ license: context.licensing.license, ruleType: type });

const rule = await updateRules({
alertsClient,
actionsClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getFindResultWithSingleHit,
getFindResultStatusEmpty,
nonRuleFindResult,
typicalMlRulePayload,
} from '../__mocks__/request_responses';
import { requestContextMock, serverMock, requestMock } from '../__mocks__';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
Expand Down Expand Up @@ -88,6 +89,22 @@ describe('update_rules', () => {
status_code: 500,
});
});

it('rejects the request if licensing is not adequate', async () => {
(context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false);
const request = requestMock.create({
method: 'put',
path: DETECTION_ENGINE_RULES_URL,
body: typicalMlRulePayload(),
});

const response = await server.inject(request, context);
expect(response.status).toEqual(400);
expect(response.body).toEqual({
message: 'Your license does not support machine learning. Please upgrade your license.',
status_code: 400,
});
});
});

describe('request validation', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import {
IRuleSavedAttributesSavedObjectAttributes,
} from '../../rules/types';
import { updateRulesSchema } from '../schemas/update_rules_schema';
import { buildRouteValidation, transformError, buildSiemResponse } from '../utils';
import {
buildRouteValidation,
transformError,
buildSiemResponse,
validateLicenseForRuleType,
} from '../utils';
import { getIdError } from './utils';
import { transformValidate } from './validate';
import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
Expand Down Expand Up @@ -66,6 +71,8 @@ export const updateRulesRoute = (router: IRouter) => {
const siemResponse = buildSiemResponse(response);

try {
validateLicenseForRuleType({ license: context.licensing.license, ruleType: type });

if (!context.alerting || !context.actions) {
return siemResponse.error({ statusCode: 404 });
}
Expand Down

0 comments on commit dd318c1

Please sign in to comment.