-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a new internal _find_statuses endpoint
- Loading branch information
Showing
4 changed files
with
201 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
...solution/server/lib/detection_engine/routes/rules/internal_find_rule_status_route.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"'); | ||
}); | ||
}); | ||
}); |
82 changes: 82 additions & 0 deletions
82
...rity_solution/server/lib/detection_engine/routes/rules/internal_find_rule_status_route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
} | ||
} | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters