Skip to content

Commit

Permalink
Add a new internal _find_statuses endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
banderror committed Nov 1, 2021
1 parent 582efaa commit 1f4d234
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 0 deletions.
7 changes: 7 additions & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,13 @@ export const DETECTION_ENGINE_RULES_PREVIEW = `${DETECTION_ENGINE_RULES_URL}/pre
export const DETECTION_ENGINE_RULES_PREVIEW_INDEX_URL =
`${DETECTION_ENGINE_RULES_PREVIEW}/index` as const;

/**
* Internal detection engine routes
*/
export const INTERNAL_DETECTION_ENGINE_URL = '/internal/detection_engine' as const;
export const INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL =
`${INTERNAL_DETECTION_ENGINE_URL}/rules/_find_statuses` as const;

export const TIMELINE_RESOLVE_URL = '/api/timeline/resolve' as const;
export const TIMELINE_URL = '/api/timeline' as const;
export const TIMELINES_URL = '/api/timelines' as const;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* 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 { INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL } from '../../../../../common/constants';
import {
ruleStatusRequest,
getAlertMock,
getRuleExecutionStatusSucceeded,
getRuleExecutionStatusFailed,
} from '../__mocks__/request_responses';
import { serverMock, requestContextMock, requestMock } from '../__mocks__';
import { findRulesStatusesRoute } from './find_rules_status_route';
import { RuleStatusResponse } from '../../rules/types';
import { AlertExecutionStatusErrorReasons } from '../../../../../../alerting/common';
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';

describe.each([
['Legacy', false],
['RAC', true],
])(`${INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL} - %s`, (_, isRuleRegistryEnabled) => {
let server: ReturnType<typeof serverMock.create>;
let { clients, context } = requestContextMock.createTools();

beforeEach(async () => {
server = serverMock.create();
({ clients, context } = requestContextMock.createTools());

clients.ruleExecutionLogClient.getCurrentStatus.mockResolvedValue(
getRuleExecutionStatusSucceeded()
);
clients.ruleExecutionLogClient.getLastFailures.mockResolvedValue([
getRuleExecutionStatusFailed(),
]);
clients.rulesClient.get.mockResolvedValue(
getAlertMock(isRuleRegistryEnabled, getQueryRuleParams())
);

findRulesStatusesRoute(server.router);
});

describe('status codes with actionClient and alertClient', () => {
test('returns 200 when finding a single rule status with a valid rulesClient', async () => {
const response = await server.inject(ruleStatusRequest(), context);
expect(response.status).toEqual(200);
});

test('returns 404 if alertClient is not available on the route', async () => {
context.alerting.getRulesClient = jest.fn();
const response = await server.inject(ruleStatusRequest(), context);
expect(response.status).toEqual(404);
expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
});

test('catch error when status search throws error', async () => {
clients.ruleExecutionLogClient.getCurrentStatus.mockImplementation(async () => {
throw new Error('Test error');
});
const response = await server.inject(ruleStatusRequest(), context);
expect(response.status).toEqual(500);
expect(response.body).toEqual({
message: 'Test error',
status_code: 500,
});
});

test('returns success if rule status client writes an error status', async () => {
// 0. task manager tried to run the rule but couldn't, so the alerting framework
// wrote an error to the executionStatus.
const failingExecutionRule = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams());
failingExecutionRule.executionStatus = {
status: 'error',
lastExecutionDate: failingExecutionRule.executionStatus.lastExecutionDate,
error: {
reason: AlertExecutionStatusErrorReasons.Read,
message: 'oops',
},
};

// 1. getFailingRules api found a rule where the executionStatus was 'error'
clients.rulesClient.get.mockResolvedValue({
...failingExecutionRule,
});

const response = await server.inject(ruleStatusRequest(), context);
const body: RuleStatusResponse = response.body;
expect(response.status).toEqual(200);
expect(body[ruleStatusRequest().body.ids[0]].current_status?.status).toEqual('failed');
expect(body[ruleStatusRequest().body.ids[0]].current_status?.last_failure_message).toEqual(
'Reason: read Message: oops'
);
});
});

describe('request validation', () => {
test('disallows singular id query param', async () => {
const request = requestMock.create({
method: 'post',
path: INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL,
body: { id: ['someId'] },
});
const result = server.validate(request);

expect(result.badRequest).toHaveBeenCalledWith('Invalid value "undefined" supplied to "ids"');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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 { transformError } from '@kbn/securitysolution-es-utils';
import { buildRouteValidation } from '../../../../utils/build_validation/route_validation';
import type { SecuritySolutionPluginRouter } from '../../../../types';
import { INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL } from '../../../../../common/constants';
import { buildSiemResponse, mergeStatuses, getFailingRules } from '../utils';
import {
findRulesStatusesSchema,
FindRulesStatusesSchemaDecoded,
} from '../../../../../common/detection_engine/schemas/request/find_rule_statuses_schema';
import { mergeAlertWithSidecarStatus } from '../../schemas/rule_converters';

/**
* Given a list of rule ids, return the current status and
* last five errors for each associated rule.
*
* @param router
* @returns RuleStatusResponse
*/
export const internalFindRuleStatusRoute = (router: SecuritySolutionPluginRouter) => {
router.post(
{
path: INTERNAL_DETECTION_ENGINE_RULE_STATUS_URL,
validate: {
body: buildRouteValidation<typeof findRulesStatusesSchema, FindRulesStatusesSchemaDecoded>(
findRulesStatusesSchema
),
},
options: {
tags: ['access:securitySolution'],
},
},
async (context, request, response) => {
const { body } = request;
const siemResponse = buildSiemResponse(response);
const rulesClient = context.alerting?.getRulesClient();

if (!rulesClient) {
return siemResponse.error({ statusCode: 404 });
}

const ruleId = body.ids[0];

try {
const ruleStatusClient = context.securitySolution.getExecutionLogClient();
const spaceId = context.securitySolution.getSpaceId();

const [currentStatus, lastFailures, failingRules] = await Promise.all([
ruleStatusClient.getCurrentStatus({ ruleId, spaceId }),
ruleStatusClient.getLastFailures({ ruleId, spaceId }),
getFailingRules([ruleId], rulesClient),
]);

const failingRule = failingRules[ruleId];
let statuses = {};

if (currentStatus != null) {
const finalCurrentStatus =
failingRule != null
? mergeAlertWithSidecarStatus(failingRule, currentStatus)
: currentStatus;

statuses = mergeStatuses(ruleId, [finalCurrentStatus, ...lastFailures], statuses);
}

return response.ok({ body: statuses });
} catch (err) {
const error = transformError(err);
return siemResponse.error({
body: error.message,
statusCode: error.statusCode,
});
}
}
);
};
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { performBulkActionRoute } from '../lib/detection_engine/routes/rules/per
import { importRulesRoute } from '../lib/detection_engine/routes/rules/import_rules_route';
import { exportRulesRoute } from '../lib/detection_engine/routes/rules/export_rules_route';
import { findRulesStatusesRoute } from '../lib/detection_engine/routes/rules/find_rules_status_route';
import { internalFindRuleStatusRoute } from '../lib/detection_engine/routes/rules/internal_find_rule_status_route';
import { getPrepackagedRulesStatusRoute } from '../lib/detection_engine/routes/rules/get_prepackaged_rules_status_route';
import {
createTimelinesRoute,
Expand Down Expand Up @@ -122,6 +123,7 @@ export const initRoutes = (
persistPinnedEventRoute(router, config, security);

findRulesStatusesRoute(router);
internalFindRuleStatusRoute(router);

// Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals
// POST /api/detection_engine/signals/status
Expand Down

0 comments on commit 1f4d234

Please sign in to comment.