[Security Solution] DetectionRulesClient: return RuleResponse from all methods (#186179)

**Partially addresses: https://github.com/elastic/kibana/issues/184364**

## Summary

This PR is a follow-up to [PR
#185748](https://github.com/elastic/kibana/pull/185748) and it converts
the remaining `DetectionRulesClient` methods to return `RuleResponse`.

Changes in this PR:
- These methods now return `RuleResponse` instead of internal
`RuleAlertType` type:
  - `updateRule`
  - `patchRule`
  - `upgradePrebuiltRule`
  - `importRule`
This commit is contained in:
Nikita Indik 2024-06-21 14:05:55 +02:00 committed by GitHub
parent 385bb2b35b
commit 55687dd539
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 224 additions and 165 deletions

View file

@ -21,7 +21,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../../types';
import { buildRouteValidation } from '../../../../../utils/build_validation/route_validation';
import type { PromisePoolError } from '../../../../../utils/promise_pool';
import { buildSiemResponse } from '../../../routes/utils';
import { internalRuleToAPIResponse } from '../../../rule_management/normalization/rule_converters';
import { aggregatePrebuiltRuleErrors } from '../../logic/aggregate_prebuilt_rule_errors';
import { performTimelinesInstallation } from '../../logic/perform_timelines_installation';
import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client';
@ -182,7 +181,7 @@ export const performRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) =>
failed: ruleErrors.length,
},
results: {
updated: updatedRules.map(({ result }) => internalRuleToAPIResponse(result)),
updated: updatedRules.map(({ result }) => result),
skipped: skippedRules,
},
errors: allErrors,

View file

@ -17,6 +17,10 @@ import {
typicalMlRulePayload,
} from '../../../../routes/__mocks__/request_responses';
import { serverMock, requestContextMock, requestMock } from '../../../../routes/__mocks__';
import {
getRulesSchemaMock,
getRulesMlSchemaMock,
} from '../../../../../../../common/api/detection_engine/model/rule_schema/rule_response_schema.mock';
import { bulkPatchRulesRoute } from './route';
import { getCreateRulesSchemaMock } from '../../../../../../../common/api/detection_engine/model/rule_schema/mocks';
import { getMlRuleParams, getQueryRuleParams } from '../../../../rule_schema/mocks';
@ -34,7 +38,7 @@ describe('Bulk patch rules route', () => {
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists
clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); // update succeeds
clients.detectionRulesClient.patchRule.mockResolvedValue(getRuleMock(getQueryRuleParams()));
clients.detectionRulesClient.patchRule.mockResolvedValue(getRulesSchemaMock());
bulkPatchRulesRoute(server.router, logger);
});
@ -72,14 +76,11 @@ describe('Bulk patch rules route', () => {
...getFindResultWithSingleHit(),
data: [getRuleMock(getMlRuleParams())],
});
clients.detectionRulesClient.patchRule.mockResolvedValueOnce(
getRuleMock(
getMlRuleParams({
anomalyThreshold,
machineLearningJobId: [machineLearningJobId],
})
)
);
clients.detectionRulesClient.patchRule.mockResolvedValueOnce({
...getRulesMlSchemaMock(),
anomaly_threshold: anomalyThreshold,
machine_learning_job_id: [machineLearningJobId],
});
const request = requestMock.create({
method: 'patch',

View file

@ -17,7 +17,6 @@ import {
import type { SecuritySolutionPluginRouter } from '../../../../../../types';
import { transformBulkError, buildSiemResponse } from '../../../../routes/utils';
import { getIdBulkError } from '../../../utils/utils';
import { transformValidateBulkError } from '../../../utils/validate';
import { readRules } from '../../../logic/detection_rules_client/read_rules';
import { getDeprecatedBulkEndpointHeader, logDeprecatedBulkEndpoint } from '../../deprecation';
import { validateRuleDefaultExceptionList } from '../../../logic/exceptions/validate_rule_default_exception_list';
@ -86,11 +85,11 @@ export const bulkPatchRulesRoute = (router: SecuritySolutionPluginRouter, logger
ruleId: payloadRule.id,
});
const rule = await detectionRulesClient.patchRule({
const patchedRule = await detectionRulesClient.patchRule({
nextParams: payloadRule,
});
return transformValidateBulkError(rule.id, rule);
return patchedRule;
} catch (err) {
return transformBulkError(idOrRuleIdOrUnknown, err);
}

View file

@ -14,6 +14,7 @@ import {
typicalMlRulePayload,
} from '../../../../routes/__mocks__/request_responses';
import { serverMock, requestContextMock, requestMock } from '../../../../routes/__mocks__';
import { getRulesSchemaMock } from '../../../../../../../common/api/detection_engine/model/rule_schema/rule_response_schema.mock';
import { bulkUpdateRulesRoute } from './route';
import type { BulkError } from '../../../../routes/utils';
import { getCreateRulesSchemaMock } from '../../../../../../../common/api/detection_engine/model/rule_schema/mocks';
@ -32,7 +33,7 @@ describe('Bulk update rules route', () => {
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams()));
clients.detectionRulesClient.updateRule.mockResolvedValue(getRuleMock(getQueryRuleParams()));
clients.detectionRulesClient.updateRule.mockResolvedValue(getRulesSchemaMock());
clients.appClient.getSignalsIndex.mockReturnValue('.siem-signals-test-index');
bulkUpdateRulesRoute(server.router, logger);

View file

@ -16,7 +16,6 @@ import {
import type { SecuritySolutionPluginRouter } from '../../../../../../types';
import { DETECTION_ENGINE_RULES_BULK_UPDATE } from '../../../../../../../common/constants';
import { getIdBulkError } from '../../../utils/utils';
import { transformValidateBulkError } from '../../../utils/validate';
import {
transformBulkError,
buildSiemResponse,
@ -97,11 +96,11 @@ export const bulkUpdateRulesRoute = (router: SecuritySolutionPluginRouter, logge
ruleId: payloadRule.id,
});
const rule = await detectionRulesClient.updateRule({
const updatedRule = await detectionRulesClient.updateRule({
ruleUpdate: payloadRule,
});
return transformValidateBulkError(rule.id, rule);
return updatedRule;
} catch (err) {
return transformBulkError(idOrRuleIdOrUnknown, err);
}

View file

@ -12,6 +12,7 @@ import {
ruleIdsToNdJsonString,
rulesToNdJsonString,
} from '../../../../../../../common/api/detection_engine/rule_management/mocks';
import { getRulesSchemaMock } from '../../../../../../../common/api/detection_engine/model/rule_schema/rule_response_schema.mock';
import type { requestMock } from '../../../../routes/__mocks__';
import { createMockConfig, requestContextMock, serverMock } from '../../../../routes/__mocks__';
@ -47,7 +48,8 @@ describe('Import rules route', () => {
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); // no extant rules
clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams()));
clients.detectionRulesClient.importRule.mockResolvedValue(getRuleMock(getQueryRuleParams()));
clients.detectionRulesClient.createCustomRule.mockResolvedValue(getRulesSchemaMock());
clients.detectionRulesClient.importRule.mockResolvedValue(getRulesSchemaMock());
clients.actionsClient.getAll.mockResolvedValue([]);
context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue(
elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse())

View file

@ -20,6 +20,11 @@ import {
import { getMlRuleParams, getQueryRuleParams } from '../../../../rule_schema/mocks';
import {
getRulesSchemaMock,
getRulesMlSchemaMock,
} from '../../../../../../../common/api/detection_engine/model/rule_schema/rule_response_schema.mock';
import { patchRuleRoute } from './route';
import { HttpAuthzError } from '../../../../../machine_learning/validation';
@ -34,7 +39,7 @@ describe('Patch rule route', () => {
clients.rulesClient.get.mockResolvedValue(getRuleMock(getQueryRuleParams())); // existing rule
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // existing rule
clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); // successful update
clients.detectionRulesClient.patchRule.mockResolvedValue(getRuleMock(getQueryRuleParams()));
clients.detectionRulesClient.patchRule.mockResolvedValue(getRulesSchemaMock());
patchRuleRoute(server.router);
});
@ -99,14 +104,11 @@ describe('Patch rule route', () => {
const anomalyThreshold = 4;
const machineLearningJobId = 'some_job_id';
clients.detectionRulesClient.patchRule.mockResolvedValueOnce(
getRuleMock(
getMlRuleParams({
anomalyThreshold,
machineLearningJobId: [machineLearningJobId],
})
)
);
clients.detectionRulesClient.patchRule.mockResolvedValueOnce({
...getRulesMlSchemaMock(),
anomaly_threshold: anomalyThreshold,
machine_learning_job_id: [machineLearningJobId],
});
const request = requestMock.create({
method: 'patch',

View file

@ -20,7 +20,6 @@ import { readRules } from '../../../logic/detection_rules_client/read_rules';
import { checkDefaultRuleExceptionListReferences } from '../../../logic/exceptions/check_for_default_rule_exception_list';
import { validateRuleDefaultExceptionList } from '../../../logic/exceptions/validate_rule_default_exception_list';
import { getIdError } from '../../../utils/utils';
import { transformValidate } from '../../../utils/validate';
export const patchRuleRoute = (router: SecuritySolutionPluginRouter) => {
router.versioned
@ -76,12 +75,12 @@ export const patchRuleRoute = (router: SecuritySolutionPluginRouter) => {
ruleId: params.id,
});
const rule = await detectionRulesClient.patchRule({
const patchedRule = await detectionRulesClient.patchRule({
nextParams: params,
});
return response.ok({
body: transformValidate(rule),
body: patchedRule,
});
} catch (err) {
const error = transformError(err);

View file

@ -13,6 +13,7 @@ import {
typicalMlRulePayload,
} from '../../../../routes/__mocks__/request_responses';
import { requestContextMock, serverMock, requestMock } from '../../../../routes/__mocks__';
import { getRulesSchemaMock } from '../../../../../../../common/api/detection_engine/model/rule_schema/rule_response_schema.mock';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../../../common/constants';
import { updateRuleRoute } from './route';
import {
@ -34,7 +35,7 @@ describe('Update rule route', () => {
clients.rulesClient.get.mockResolvedValue(getRuleMock(getQueryRuleParams())); // existing rule
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists
clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); // successful update
clients.detectionRulesClient.updateRule.mockResolvedValue(getRuleMock(getQueryRuleParams()));
clients.detectionRulesClient.updateRule.mockResolvedValue(getRulesSchemaMock());
clients.appClient.getSignalsIndex.mockReturnValue('.siem-signals-test-index');
updateRuleRoute(server.router);

View file

@ -20,7 +20,7 @@ import { readRules } from '../../../logic/detection_rules_client/read_rules';
import { checkDefaultRuleExceptionListReferences } from '../../../logic/exceptions/check_for_default_rule_exception_list';
import { validateRuleDefaultExceptionList } from '../../../logic/exceptions/validate_rule_default_exception_list';
import { getIdError } from '../../../utils/utils';
import { transformValidate, validateResponseActionsPermissions } from '../../../utils/validate';
import { validateResponseActionsPermissions } from '../../../utils/validate';
export const updateRuleRoute = (router: SecuritySolutionPluginRouter) => {
router.versioned
@ -80,12 +80,12 @@ export const updateRuleRoute = (router: SecuritySolutionPluginRouter) => {
existingRule
);
const rule = await detectionRulesClient.updateRule({
const updatedRule = await detectionRulesClient.updateRule({
ruleUpdate: request.body,
});
return response.ok({
body: transformValidate(rule),
body: updatedRule,
});
} catch (err) {
const error = transformError(err);

View file

@ -19,8 +19,6 @@ import { buildMlAuthz } from '../../../../machine_learning/authz';
import { throwAuthzError } from '../../../../machine_learning/validation';
import { createDetectionRulesClient } from './detection_rules_client';
import type { IDetectionRulesClient } from './detection_rules_client_interface';
import { RuleResponseValidationError } from './utils';
import type { RuleAlertType } from '../../../rule_schema';
jest.mock('../../../../machine_learning/authz');
jest.mock('../../../../machine_learning/validation');
@ -70,20 +68,6 @@ describe('DetectionRulesClient.createCustomRule', () => {
expect(rulesClient.create).not.toHaveBeenCalled();
});
it('throws if RuleResponse validation fails', async () => {
const internalRuleMock: RuleAlertType = getRuleMock({
...getQueryRuleParams(),
/* Casting as 'query' suppress to TS error */
type: 'fake-non-existent-type' as 'query',
});
rulesClient.create.mockResolvedValueOnce(internalRuleMock);
await expect(
detectionRulesClient.createCustomRule({ params: getCreateMachineLearningRulesSchemaMock() })
).rejects.toThrow(RuleResponseValidationError);
});
it('calls the rulesClient with legacy ML params', async () => {
await detectionRulesClient.createCustomRule({
params: getCreateMachineLearningRulesSchemaMock(),

View file

@ -42,6 +42,8 @@ describe('DetectionRulesClient.importRule', () => {
beforeEach(() => {
rulesClient = rulesClientMock.create();
rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams()));
rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams()));
detectionRulesClient = createDetectionRulesClient(rulesClient, mlAuthz);
});

View file

@ -8,7 +8,6 @@
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { MlAuthz } from '../../../../machine_learning/authz';
import type { RuleAlertType } from '../../../rule_schema';
import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema';
import type {
IDetectionRulesClient,
@ -47,13 +46,13 @@ export const createDetectionRulesClient = (
});
},
async updateRule(args: UpdateRuleArgs): Promise<RuleAlertType> {
async updateRule(args: UpdateRuleArgs): Promise<RuleResponse> {
return withSecuritySpan('DetectionRulesClient.updateRule', async () => {
return updateRule(rulesClient, args, mlAuthz);
});
},
async patchRule(args: PatchRuleArgs): Promise<RuleAlertType> {
async patchRule(args: PatchRuleArgs): Promise<RuleResponse> {
return withSecuritySpan('DetectionRulesClient.patchRule', async () => {
return patchRule(rulesClient, args, mlAuthz);
});
@ -65,13 +64,13 @@ export const createDetectionRulesClient = (
});
},
async upgradePrebuiltRule(args: UpgradePrebuiltRuleArgs): Promise<RuleAlertType> {
async upgradePrebuiltRule(args: UpgradePrebuiltRuleArgs): Promise<RuleResponse> {
return withSecuritySpan('DetectionRulesClient.upgradePrebuiltRule', async () => {
return upgradePrebuiltRule(rulesClient, args, mlAuthz);
});
},
async importRule(args: ImportRuleArgs): Promise<RuleAlertType> {
async importRule(args: ImportRuleArgs): Promise<RuleResponse> {
return withSecuritySpan('DetectionRulesClient.importRule', async () => {
return importRule(rulesClient, args, mlAuthz);
});

View file

@ -99,10 +99,12 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => {
ruleId: 'rule-id',
});
beforeEach(() => {
jest.resetAllMocks();
rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams()));
(readRules as jest.Mock).mockResolvedValue(installedRule);
});
it('deletes the old rule ', async () => {
it('deletes the old rule', async () => {
await detectionRulesClient.upgradePrebuiltRule({ ruleAsset });
expect(rulesClient.delete).toHaveBeenCalled();
});
@ -153,6 +155,8 @@ describe('DetectionRulesClient.upgradePrebuiltRule', () => {
});
it('patches the existing rule with the new params from the rule asset', async () => {
rulesClient.update.mockResolvedValue(getRuleMock(getEqlRuleParams()));
await detectionRulesClient.upgradePrebuiltRule({ ruleAsset });
expect(rulesClient.update).toHaveBeenCalledWith(
expect.objectContaining({

View file

@ -13,17 +13,16 @@ import type {
RuleToImport,
RuleResponse,
} from '../../../../../../common/api/detection_engine';
import type { RuleAlertType } from '../../../rule_schema';
import type { PrebuiltRuleAsset } from '../../../prebuilt_rules';
export interface IDetectionRulesClient {
createCustomRule: (args: CreateCustomRuleArgs) => Promise<RuleResponse>;
createPrebuiltRule: (args: CreatePrebuiltRuleArgs) => Promise<RuleResponse>;
updateRule: (args: UpdateRuleArgs) => Promise<RuleAlertType>;
patchRule: (args: PatchRuleArgs) => Promise<RuleAlertType>;
updateRule: (args: UpdateRuleArgs) => Promise<RuleResponse>;
patchRule: (args: PatchRuleArgs) => Promise<RuleResponse>;
deleteRule: (args: DeleteRuleArgs) => Promise<void>;
upgradePrebuiltRule: (args: UpgradePrebuiltRuleArgs) => Promise<RuleAlertType>;
importRule: (args: ImportRuleArgs) => Promise<RuleAlertType>;
upgradePrebuiltRule: (args: UpgradePrebuiltRuleArgs) => Promise<RuleResponse>;
importRule: (args: ImportRuleArgs) => Promise<RuleResponse>;
}
export interface CreateCustomRuleArgs {

View file

@ -11,8 +11,10 @@ import type { CreateCustomRuleArgs } from '../detection_rules_client_interface';
import type { MlAuthz } from '../../../../../machine_learning/authz';
import type { RuleParams } from '../../../../rule_schema';
import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
import { convertCreateAPIToInternalSchema } from '../../../normalization/rule_converters';
import { transform } from '../../../utils/utils';
import {
convertCreateAPIToInternalSchema,
internalRuleToAPIResponse,
} from '../../../normalization/rule_converters';
import { validateMlAuth, RuleResponseValidationError } from '../utils';
export const createCustomRule = async (
@ -29,7 +31,7 @@ export const createCustomRule = async (
});
/* Trying to convert the rule to a RuleResponse object */
const parseResult = RuleResponse.safeParse(transform(rule));
const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(rule));
if (!parseResult.success) {
throw new RuleResponseValidationError({

View file

@ -11,8 +11,10 @@ import type { CreatePrebuiltRuleArgs } from '../detection_rules_client_interface
import type { MlAuthz } from '../../../../../machine_learning/authz';
import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
import type { RuleParams } from '../../../../rule_schema';
import { convertCreateAPIToInternalSchema } from '../../../normalization/rule_converters';
import { transform } from '../../../utils/utils';
import {
convertCreateAPIToInternalSchema,
internalRuleToAPIResponse,
} from '../../../normalization/rule_converters';
import { validateMlAuth, RuleResponseValidationError } from '../utils';
export const createPrebuiltRule = async (
@ -34,7 +36,7 @@ export const createPrebuiltRule = async (
});
/* Trying to convert the rule to a RuleResponse object */
const parseResult = RuleResponse.safeParse(transform(rule));
const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(rule));
if (!parseResult.success) {
throw new RuleResponseValidationError({

View file

@ -6,6 +6,7 @@
*/
import type { RulesClient } from '@kbn/alerting-plugin/server';
import { stringifyZodError } from '@kbn/zod-helpers';
import type { MlAuthz } from '../../../../../machine_learning/authz';
import type { ImportRuleArgs } from '../detection_rules_client_interface';
import type { RuleAlertType, RuleParams } from '../../../../rule_schema';
@ -13,9 +14,11 @@ import { createBulkErrorObject } from '../../../../routes/utils';
import {
convertCreateAPIToInternalSchema,
convertUpdateAPIToInternalSchema,
internalRuleToAPIResponse,
} from '../../../normalization/rule_converters';
import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
import { validateMlAuth } from '../utils';
import { validateMlAuth, RuleResponseValidationError } from '../utils';
import { readRules } from '../read_rules';
@ -23,7 +26,7 @@ export const importRule = async (
rulesClient: RulesClient,
importRulePayload: ImportRuleArgs,
mlAuthz: MlAuthz
): Promise<RuleAlertType> => {
): Promise<RuleResponse> => {
const { ruleToImport, overwriteRules, allowMissingConnectorSecrets } = importRulePayload;
await validateMlAuth(mlAuthz, ruleToImport.type);
@ -34,30 +37,47 @@ export const importRule = async (
id: undefined,
});
if (!existingRule) {
const internalRule = convertCreateAPIToInternalSchema(ruleToImport, {
immutable: false,
});
return rulesClient.create<RuleParams>({
data: internalRule,
allowMissingConnectorSecrets,
});
} else if (existingRule && overwriteRules) {
const newInternalRule = convertUpdateAPIToInternalSchema({
existingRule,
ruleUpdate: ruleToImport,
});
return rulesClient.update({
id: existingRule.id,
data: newInternalRule,
});
} else {
if (existingRule && !overwriteRules) {
throw createBulkErrorObject({
ruleId: existingRule.params.ruleId,
statusCode: 409,
message: `rule_id: "${existingRule.params.ruleId}" already exists`,
});
}
let importedInternalRule: RuleAlertType;
if (existingRule && overwriteRules) {
const ruleUpdateParams = convertUpdateAPIToInternalSchema({
existingRule,
ruleUpdate: ruleToImport,
});
importedInternalRule = await rulesClient.update({
id: existingRule.id,
data: ruleUpdateParams,
});
} else {
/* Rule does not exist, so we'll create it */
const ruleCreateParams = convertCreateAPIToInternalSchema(ruleToImport, {
immutable: false,
});
importedInternalRule = await rulesClient.create<RuleParams>({
data: ruleCreateParams,
allowMissingConnectorSecrets,
});
}
/* Trying to convert an internal rule to a RuleResponse object */
const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(importedInternalRule));
if (!parseResult.success) {
throw new RuleResponseValidationError({
message: stringifyZodError(parseResult.error),
ruleId: importedInternalRule.params.ruleId,
});
}
return parseResult.data;
};

View file

@ -6,13 +6,22 @@
*/
import type { RulesClient } from '@kbn/alerting-plugin/server';
import { stringifyZodError } from '@kbn/zod-helpers';
import type { MlAuthz } from '../../../../../machine_learning/authz';
import type { PatchRuleArgs } from '../detection_rules_client_interface';
import type { RuleAlertType } from '../../../../rule_schema';
import { getIdError } from '../../../utils/utils';
import { convertPatchAPIToInternalSchema } from '../../../normalization/rule_converters';
import {
convertPatchAPIToInternalSchema,
internalRuleToAPIResponse,
} from '../../../normalization/rule_converters';
import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
import { validateMlAuth, ClientError, toggleRuleEnabledOnUpdate } from '../utils';
import {
validateMlAuth,
ClientError,
toggleRuleEnabledOnUpdate,
RuleResponseValidationError,
} from '../utils';
import { readRules } from '../read_rules';
@ -20,7 +29,7 @@ export const patchRule = async (
rulesClient: RulesClient,
args: PatchRuleArgs,
mlAuthz: MlAuthz
): Promise<RuleAlertType> => {
): Promise<RuleResponse> => {
const { nextParams } = args;
const { rule_id: ruleId, id } = nextParams;
@ -39,16 +48,28 @@ export const patchRule = async (
const patchedRule = convertPatchAPIToInternalSchema(nextParams, existingRule);
const update = await rulesClient.update({
const patchedInternalRule = await rulesClient.update({
id: existingRule.id,
data: patchedRule,
});
await toggleRuleEnabledOnUpdate(rulesClient, existingRule, nextParams.enabled);
const { enabled } = await toggleRuleEnabledOnUpdate(
rulesClient,
existingRule,
nextParams.enabled
);
if (nextParams.enabled != null) {
return { ...update, enabled: nextParams.enabled };
} else {
return update;
/* Trying to convert the internal rule to a RuleResponse object */
const parseResult = RuleResponse.safeParse(
internalRuleToAPIResponse({ ...patchedInternalRule, enabled })
);
if (!parseResult.success) {
throw new RuleResponseValidationError({
message: stringifyZodError(parseResult.error),
ruleId: patchedInternalRule.params.ruleId,
});
}
return parseResult.data;
};

View file

@ -6,13 +6,22 @@
*/
import type { RulesClient } from '@kbn/alerting-plugin/server';
import { stringifyZodError } from '@kbn/zod-helpers';
import type { MlAuthz } from '../../../../../machine_learning/authz';
import type { RuleAlertType } from '../../../../rule_schema';
import type { UpdateRuleArgs } from '../detection_rules_client_interface';
import { getIdError } from '../../../utils/utils';
import { convertUpdateAPIToInternalSchema } from '../../../normalization/rule_converters';
import {
convertUpdateAPIToInternalSchema,
internalRuleToAPIResponse,
} from '../../../normalization/rule_converters';
import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
import { validateMlAuth, ClientError, toggleRuleEnabledOnUpdate } from '../utils';
import {
validateMlAuth,
ClientError,
toggleRuleEnabledOnUpdate,
RuleResponseValidationError,
} from '../utils';
import { readRules } from '../read_rules';
@ -20,7 +29,7 @@ export const updateRule = async (
rulesClient: RulesClient,
args: UpdateRuleArgs,
mlAuthz: MlAuthz
): Promise<RuleAlertType> => {
): Promise<RuleResponse> => {
const { ruleUpdate } = args;
const { rule_id: ruleId, id } = ruleUpdate;
@ -42,12 +51,28 @@ export const updateRule = async (
ruleUpdate,
});
const update = await rulesClient.update({
const updatedInternalRule = await rulesClient.update({
id: existingRule.id,
data: newInternalRule,
});
await toggleRuleEnabledOnUpdate(rulesClient, existingRule, ruleUpdate.enabled);
const { enabled } = await toggleRuleEnabledOnUpdate(
rulesClient,
existingRule,
ruleUpdate.enabled
);
return { ...update, enabled: ruleUpdate.enabled ?? existingRule.enabled };
/* Trying to convert the internal rule to a RuleResponse object */
const parseResult = RuleResponse.safeParse(
internalRuleToAPIResponse({ ...updatedInternalRule, enabled })
);
if (!parseResult.success) {
throw new RuleResponseValidationError({
message: stringifyZodError(parseResult.error),
ruleId: updatedInternalRule.params.ruleId,
});
}
return parseResult.data;
};

View file

@ -6,16 +6,19 @@
*/
import type { RulesClient } from '@kbn/alerting-plugin/server';
import { stringifyZodError } from '@kbn/zod-helpers';
import type { MlAuthz } from '../../../../../machine_learning/authz';
import type { RuleAlertType, RuleParams } from '../../../../rule_schema';
import type { RuleParams } from '../../../../rule_schema';
import type { UpgradePrebuiltRuleArgs } from '../detection_rules_client_interface';
import {
convertPatchAPIToInternalSchema,
convertCreateAPIToInternalSchema,
internalRuleToAPIResponse,
} from '../../../normalization/rule_converters';
import { transformAlertToRuleAction } from '../../../../../../../common/detection_engine/transform_actions';
import { RuleResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema';
import { validateMlAuth, ClientError } from '../utils';
import { validateMlAuth, ClientError, RuleResponseValidationError } from '../utils';
import { readRules } from '../read_rules';
@ -23,7 +26,7 @@ export const upgradePrebuiltRule = async (
rulesClient: RulesClient,
upgradePrebuiltRulePayload: UpgradePrebuiltRuleArgs,
mlAuthz: MlAuthz
): Promise<RuleAlertType> => {
): Promise<RuleResponse> => {
const { ruleAsset } = upgradePrebuiltRulePayload;
await validateMlAuth(mlAuthz, ruleAsset.type);
@ -56,29 +59,41 @@ export const upgradePrebuiltRule = async (
{ immutable: true, defaultEnabled: existingRule.enabled }
);
return rulesClient.create<RuleParams>({
const createdRule = await rulesClient.create<RuleParams>({
data: internalRule,
options: { id: existingRule.id },
});
/* Trying to convert the rule to a RuleResponse object */
const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(createdRule));
if (!parseResult.success) {
throw new RuleResponseValidationError({
message: stringifyZodError(parseResult.error),
ruleId: createdRule.params.ruleId,
});
}
return parseResult.data;
}
// Else, simply patch it.
const patchedRule = convertPatchAPIToInternalSchema(ruleAsset, existingRule);
await rulesClient.update({
const patchedInternalRule = await rulesClient.update({
id: existingRule.id,
data: patchedRule,
});
const updatedRule = await readRules({
rulesClient,
ruleId: ruleAsset.rule_id,
id: undefined,
});
/* Trying to convert the internal rule to a RuleResponse object */
const parseResult = RuleResponse.safeParse(internalRuleToAPIResponse(patchedInternalRule));
if (!updatedRule) {
throw new ClientError(`Rule ${ruleAsset.rule_id} not found after upgrade`, 500);
if (!parseResult.success) {
throw new RuleResponseValidationError({
message: stringifyZodError(parseResult.error),
ruleId: patchedInternalRule.params.ruleId,
});
}
return updatedRule;
return parseResult.data;
};

View file

@ -20,12 +20,18 @@ export const toggleRuleEnabledOnUpdate = async (
rulesClient: RulesClient,
existingRule: RuleAlertType,
updatedRuleEnabled?: boolean
) => {
): Promise<{ enabled: boolean }> => {
if (existingRule.enabled && updatedRuleEnabled === false) {
await rulesClient.disable({ id: existingRule.id });
} else if (!existingRule.enabled && updatedRuleEnabled === true) {
await rulesClient.enable({ id: existingRule.id });
return { enabled: false };
}
if (!existingRule.enabled && updatedRuleEnabled === true) {
await rulesClient.enable({ id: existingRule.id });
return { enabled: true };
}
return { enabled: existingRule.enabled };
};
export const validateMlAuth = async (mlAuthz: MlAuthz, ruleType: Type) => {

View file

@ -6,28 +6,21 @@
*/
import { getImportRulesSchemaMock } from '../../../../../../common/api/detection_engine/rule_management/mocks';
import { getQueryRuleParams } from '../../../rule_schema/mocks';
import { getRulesSchemaMock } from '../../../../../../common/api/detection_engine/model/rule_schema/rule_response_schema.mock';
import { requestContextMock } from '../../../routes/__mocks__';
import { getRuleMock, getEmptyFindResult } from '../../../routes/__mocks__/request_responses';
import { importRules } from './import_rules_utils';
import { createBulkErrorObject } from '../../../routes/utils';
describe('importRules', () => {
const { clients, context } = requestContextMock.createTools();
const importedRule = getRuleMock(getQueryRuleParams());
const ruleToImport = getImportRulesSchemaMock();
beforeEach(() => {
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult());
clients.rulesClient.update.mockResolvedValue(importedRule);
clients.detectionRulesClient.importRule.mockResolvedValue(importedRule);
clients.actionsClient.getAll.mockResolvedValue([]);
jest.clearAllMocks();
});
it('returns rules response if no rules to import', async () => {
it('returns an empty rules response if no rules to import', async () => {
const result = await importRules({
ruleChunks: [],
rulesResponseAcc: [],
@ -62,12 +55,13 @@ describe('importRules', () => {
it('returns 409 error if DetectionRulesClient throws with 409 - existing rule', async () => {
clients.detectionRulesClient.importRule.mockImplementationOnce(async () => {
throw createBulkErrorObject({
ruleId: importedRule.params.ruleId,
ruleId: ruleToImport.rule_id,
statusCode: 409,
message: `rule_id: "${importedRule.params.ruleId}" already exists`,
message: `rule_id: "${ruleToImport.rule_id}" already exists`,
});
});
const ruleChunk = [getImportRulesSchemaMock({ rule_id: importedRule.params.ruleId })];
const ruleChunk = [ruleToImport];
const result = await importRules({
ruleChunks: [ruleChunk],
rulesResponseAcc: [],
@ -79,15 +73,21 @@ describe('importRules', () => {
expect(result).toEqual([
{
error: {
message: `rule_id: "${importedRule.params.ruleId}" already exists`,
message: `rule_id: "${ruleToImport.rule_id}" already exists`,
status_code: 409,
},
rule_id: importedRule.params.ruleId,
rule_id: ruleToImport.rule_id,
},
]);
});
it('creates rule if no matching existing rule found', async () => {
const ruleChunk = [getImportRulesSchemaMock({ rule_id: importedRule.params.ruleId })];
clients.detectionRulesClient.importRule.mockResolvedValue({
...getRulesSchemaMock(),
rule_id: ruleToImport.rule_id,
});
const ruleChunk = [ruleToImport];
const result = await importRules({
ruleChunks: [ruleChunk],
rulesResponseAcc: [],
@ -96,6 +96,6 @@ describe('importRules', () => {
existingLists: {},
});
expect(result).toEqual([{ rule_id: importedRule.params.ruleId, status_code: 200 }]);
expect(result).toEqual([{ rule_id: ruleToImport.rule_id, status_code: 200 }]);
});
});

View file

@ -98,7 +98,7 @@ export const importRules = async ({
});
resolve({
rule_id: importedRule.params.ruleId,
rule_id: importedRule.rule_id,
status_code: 200,
});
} catch (err) {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { transformValidate, transformValidateBulkError } from './validate';
import { transformValidateBulkError } from './validate';
import type { BulkError } from '../../routes/utils';
import { getRuleMock } from '../../routes/__mocks__/request_responses';
import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock';
@ -82,23 +82,6 @@ export const ruleOutput = (): RuleResponse => ({
});
describe('validate', () => {
describe('transformValidate', () => {
test('it should do a validation correctly of a partial alert', () => {
const ruleAlert = getRuleMock(getQueryRuleParams());
const validated = transformValidate(ruleAlert);
expect(validated).toEqual(ruleOutput());
});
test('it should do an in-validation correctly of a partial alert', () => {
const ruleAlert = getRuleMock(getQueryRuleParams());
// @ts-expect-error
delete ruleAlert.name;
expect(() => {
transformValidate(ruleAlert);
}).toThrowError('Required');
});
});
describe('transformValidateBulkError', () => {
test('it should do a validation correctly of a rule id', () => {
const ruleAlert = getRuleMock(getQueryRuleParams());

View file

@ -31,14 +31,8 @@ import {
type UnifiedQueryRuleParams,
} from '../../rule_schema';
import { type BulkError, createBulkErrorObject } from '../../routes/utils';
import { transform } from './utils';
import { internalRuleToAPIResponse } from '../normalization/rule_converters';
export const transformValidate = (rule: PartialRule<RuleParams>): RuleResponse => {
const transformed = transform(rule);
return RuleResponse.parse(transformed);
};
export const transformValidateBulkError = (
ruleId: string,
rule: PartialRule<RuleParams>