Skip to content

Commit

Permalink
[8.x] [Security Solution] Prevent non-customizable fields from updati…
Browse files Browse the repository at this point in the history
…ng for Prebuilt rule types (#195318) (#195837)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Security Solution] Prevent non-customizable fields from updating for
Prebuilt rule types
(#195318)](#195318)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Davis
Plumlee","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-10-10T22:48:47Z","message":"[Security
Solution] Prevent non-customizable fields from updating for Prebuilt
rule types (#195318)\n\n## Summary\r\n\r\nAddresses
https://github.com/elastic/kibana/issues/180273\r\n\r\nAdds validation
in the `detectionRulesClient` to prevent the updating
of\r\nnon-customizable fields in Prebuilt rule types (i.e.
external\r\n`rule_source`). Returns a `400` error if `author` or
`license` fields\r\nare updated via `PUT` and `PATCH` endpoints for
external rules.\r\n\r\nAlso updates related test utils to reflect this
new logic\r\n\r\n### Checklist\r\n\r\nDelete any items that are not
applicable to this PR.\r\n\r\n- [
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n\r\n\r\n### For maintainers\r\n\r\n-
[ ] This was checked for breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"00042177a8e976d379b5e40db3664db1e333999d","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:Detections
and Resp","Team: SecuritySolution","Team:Detection Rule
Management","Feature:Prebuilt Detection
Rules","backport:prev-minor","v8.16.0"],"title":"[Security Solution]
Prevent non-customizable fields from updating for Prebuilt rule
types","number":195318,"url":"https://github.com/elastic/kibana/pull/195318","mergeCommit":{"message":"[Security
Solution] Prevent non-customizable fields from updating for Prebuilt
rule types (#195318)\n\n## Summary\r\n\r\nAddresses
https://github.com/elastic/kibana/issues/180273\r\n\r\nAdds validation
in the `detectionRulesClient` to prevent the updating
of\r\nnon-customizable fields in Prebuilt rule types (i.e.
external\r\n`rule_source`). Returns a `400` error if `author` or
`license` fields\r\nare updated via `PUT` and `PATCH` endpoints for
external rules.\r\n\r\nAlso updates related test utils to reflect this
new logic\r\n\r\n### Checklist\r\n\r\nDelete any items that are not
applicable to this PR.\r\n\r\n- [
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n\r\n\r\n### For maintainers\r\n\r\n-
[ ] This was checked for breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"00042177a8e976d379b5e40db3664db1e333999d"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/195318","number":195318,"mergeCommit":{"message":"[Security
Solution] Prevent non-customizable fields from updating for Prebuilt
rule types (#195318)\n\n## Summary\r\n\r\nAddresses
https://github.com/elastic/kibana/issues/180273\r\n\r\nAdds validation
in the `detectionRulesClient` to prevent the updating
of\r\nnon-customizable fields in Prebuilt rule types (i.e.
external\r\n`rule_source`). Returns a `400` error if `author` or
`license` fields\r\nare updated via `PUT` and `PATCH` endpoints for
external rules.\r\n\r\nAlso updates related test utils to reflect this
new logic\r\n\r\n### Checklist\r\n\r\nDelete any items that are not
applicable to this PR.\r\n\r\n- [
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n\r\n\r\n### For maintainers\r\n\r\n-
[ ] This was checked for breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"00042177a8e976d379b5e40db3664db1e333999d"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Davis Plumlee <[email protected]>
  • Loading branch information
kibanamachine and dplumlee authored Oct 11, 2024
1 parent 3521b8c commit 58dc313
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const getPrebuiltRuleMock = (rewrites?: Partial<PrebuiltRuleAsset>): Preb
language: 'kuery',
rule_id: 'rule-1',
version: 1,
author: [],
...rewrites,
} as PrebuiltRuleAsset);

Expand Down Expand Up @@ -51,6 +52,7 @@ export const getPrebuiltThreatMatchRuleMock = (): PrebuiltRuleAsset => ({
language: 'kuery',
rule_id: 'rule-1',
version: 1,
author: [],
threat_query: '*:*',
threat_index: ['list-index'],
threat_mapping: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,27 @@ describe('DetectionRulesClient.patchRule', () => {
expect(rulesClient.create).not.toHaveBeenCalled();
});

it('throws an error if rule has external rule source and non-customizable fields are changed', async () => {
// Mock the existing rule
const existingRule = {
...getRulesSchemaMock(),
rule_source: { type: 'external', is_customized: true },
};
(getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule);

// Mock the rule update
const rulePatch = getCreateRulesSchemaMock('query-rule-id');
rulePatch.license = 'new license';

// Mock the rule returned after update; not used for this test directly but
// needed so that the patchRule method does not throw
rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams()));

await expect(detectionRulesClient.patchRule({ rulePatch })).rejects.toThrow(
'Cannot update "license" field for prebuilt rules'
);
});

describe('actions', () => {
it("updates the rule's actions if provided", async () => {
// Mock the existing rule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,5 +498,26 @@ describe('DetectionRulesClient.updateRule', () => {
})
);
});

it('throws an error if rule has external rule source and non-customizable fields are changed', async () => {
// Mock the existing rule
const existingRule = {
...getRulesSchemaMock(),
rule_source: { type: 'external', is_customized: true },
};

(getRuleByRuleId as jest.Mock).mockResolvedValueOnce(existingRule);

// Mock the rule update
const ruleUpdate = { ...getCreateRulesSchemaMock(), author: ['new user'] };

// Mock the rule returned after update; not used for this test directly but
// needed so that the patchRule method does not throw
rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams()));

await expect(detectionRulesClient.updateRule({ ruleUpdate })).rejects.toThrow(
'Cannot update "author" field for prebuilt rules'
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { MlAuthz } from '../../../../../machine_learning/authz';
import type { IPrebuiltRuleAssetsClient } from '../../../../prebuilt_rules/logic/rule_assets/prebuilt_rule_assets_client';
import { applyRulePatch } from '../mergers/apply_rule_patch';
import { getIdError } from '../../../utils/utils';
import { validateNonCustomizablePatchFields } from '../../../utils/validate';
import { convertAlertingRuleToRuleResponse } from '../converters/convert_alerting_rule_to_rule_response';
import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule';
import { ClientError, toggleRuleEnabledOnUpdate, validateMlAuth } from '../utils';
Expand Down Expand Up @@ -51,6 +52,8 @@ export const patchRule = async ({

await validateMlAuth(mlAuthz, rulePatch.type ?? existingRule.type);

validateNonCustomizablePatchFields(rulePatch, existingRule);

const patchedRule = await applyRulePatch({
prebuiltRuleAssetClient,
existingRule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { RuleResponse } from '../../../../../../../common/api/detection_eng
import type { MlAuthz } from '../../../../../machine_learning/authz';
import { applyRuleUpdate } from '../mergers/apply_rule_update';
import { getIdError } from '../../../utils/utils';
import { validateNonCustomizableUpdateFields } from '../../../utils/validate';
import { convertRuleResponseToAlertingRule } from '../converters/convert_rule_response_to_alerting_rule';

import { ClientError, toggleRuleEnabledOnUpdate, validateMlAuth } from '../utils';
Expand Down Expand Up @@ -50,6 +51,8 @@ export const updateRule = async ({
throw new ClientError(error.message, error.statusCode);
}

validateNonCustomizableUpdateFields(ruleUpdate, existingRule);

const ruleWithUpdates = await applyRuleUpdate({
prebuiltRuleAssetClient,
existingRule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
RuleResponse,
type RuleResponseAction,
type RuleUpdateProps,
type RulePatchProps,
} from '../../../../../common/api/detection_engine';
import {
RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP,
Expand All @@ -25,6 +26,7 @@ import { CustomHttpRequestError } from '../../../../utils/custom_http_request_er
import { hasValidRuleType, type RuleAlertType, type RuleParams } from '../../rule_schema';
import { type BulkError, createBulkErrorObject } from '../../routes/utils';
import { internalRuleToAPIResponse } from '../logic/detection_rules_client/converters/internal_rule_to_api_response';
import { ClientError } from '../logic/detection_rules_client/utils';

export const transformValidateBulkError = (
ruleId: string,
Expand Down Expand Up @@ -117,3 +119,31 @@ function rulePayloadContainsResponseActions(rule: RuleCreateProps | RuleUpdatePr
function ruleObjectContainsResponseActions(rule?: RuleAlertType) {
return rule != null && 'params' in rule && 'responseActions' in rule?.params;
}

export const validateNonCustomizableUpdateFields = (
ruleUpdate: RuleUpdateProps,
existingRule: RuleResponse
) => {
// We don't allow non-customizable fields to be changed for prebuilt rules
if (existingRule.rule_source && existingRule.rule_source.type === 'external') {
if (!isEqual(ruleUpdate.author, existingRule.author)) {
throw new ClientError(`Cannot update "author" field for prebuilt rules`, 400);
} else if (ruleUpdate.license !== existingRule.license) {
throw new ClientError(`Cannot update "license" field for prebuilt rules`, 400);
}
}
};

export const validateNonCustomizablePatchFields = (
rulePatch: RulePatchProps,
existingRule: RuleResponse
) => {
// We don't allow non-customizable fields to be changed for prebuilt rules
if (existingRule.rule_source && existingRule.rule_source.type === 'external') {
if (rulePatch.author && !isEqual(rulePatch.author, existingRule.author)) {
throw new ClientError(`Cannot update "author" field for prebuilt rules`, 400);
} else if (rulePatch.license != null && rulePatch.license !== existingRule.license) {
throw new ClientError(`Cannot update "license" field for prebuilt rules`, 400);
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
removeServerGeneratedPropertiesIncludingRuleId,
getSimpleRuleOutputWithoutRuleId,
updateUsername,
createHistoricalPrebuiltRuleAssetSavedObjects,
installPrebuiltRules,
createRuleAssetSavedObject,
} from '../../../utils';
import {
createAlertsIndex,
Expand Down Expand Up @@ -238,6 +241,25 @@ export default ({ getService }: FtrProviderContext) => {
});
});

it('throws an error if rule has external rule source and non-customizable fields are changed', async () => {
// Install base prebuilt detection rule
await createHistoricalPrebuiltRuleAssetSavedObjects(es, [
createRuleAssetSavedObject({ rule_id: 'rule-1', author: ['elastic'] }),
]);
await installPrebuiltRules(es, supertest);

const { body } = await securitySolutionApi
.patchRule({
body: {
rule_id: 'rule-1',
author: ['new user'],
},
})
.expect(400);

expect(body.message).toEqual('Cannot update "author" field for prebuilt rules');
});

describe('max signals', () => {
afterEach(async () => {
await deleteAllRules(supertest, log);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
getSimpleRuleOutputWithoutRuleId,
removeServerGeneratedPropertiesIncludingRuleId,
updateUsername,
createHistoricalPrebuiltRuleAssetSavedObjects,
installPrebuiltRules,
createRuleAssetSavedObject,
} from '../../../utils';
import {
createAlertsIndex,
Expand Down Expand Up @@ -347,6 +350,41 @@ export default ({ getService }: FtrProviderContext) => {
},
]);
});

it('throws an error if rule has external rule source and non-customizable fields are changed', async () => {
// Install base prebuilt detection rule
await createHistoricalPrebuiltRuleAssetSavedObjects(es, [
createRuleAssetSavedObject({ rule_id: 'rule-1', author: ['elastic'] }),
createRuleAssetSavedObject({ rule_id: 'rule-2', license: 'basic' }),
]);
await installPrebuiltRules(es, supertest);

const { body } = await securitySolutionApi
.bulkPatchRules({
body: [
{ rule_id: 'rule-1', author: ['new user'] },
{ rule_id: 'rule-2', license: 'new license' },
],
})
.expect(200);

expect([body[0], body[1]]).toEqual([
{
error: {
message: 'Cannot update "author" field for prebuilt rules',
status_code: 400,
},
rule_id: 'rule-1',
},
{
error: {
message: 'Cannot update "license" field for prebuilt rules',
status_code: 400,
},
rule_id: 'rule-2',
},
]);
});
});
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import {
getSimpleMlRuleUpdate,
getSimpleRule,
updateUsername,
createHistoricalPrebuiltRuleAssetSavedObjects,
installPrebuiltRules,
createRuleAssetSavedObject,
} from '../../../utils';
import {
createAlertsIndex,
Expand Down Expand Up @@ -309,6 +312,33 @@ export default ({ getService }: FtrProviderContext) => {
expect(updatedRuleResponse).toMatchObject(expectedRule);
});
});

it('throws an error if rule has external rule source and non-customizable fields are changed', async () => {
// Install base prebuilt detection rule
await createHistoricalPrebuiltRuleAssetSavedObjects(es, [
createRuleAssetSavedObject({ rule_id: 'rule-1', license: 'elastic' }),
]);
await installPrebuiltRules(es, supertest);

const { body: existingRule } = await securitySolutionApi
.readRule({
query: { rule_id: 'rule-1' },
})
.expect(200);

const { body } = await securitySolutionApi
.updateRule({
body: getCustomQueryRuleParams({
...existingRule,
rule_id: 'rule-1',
id: undefined,
license: 'new license',
}),
})
.expect(400);

expect(body.message).toEqual('Cannot update "license" field for prebuilt rules');
});
});
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import {
getSimpleRuleUpdate,
getSimpleRule,
updateUsername,
createHistoricalPrebuiltRuleAssetSavedObjects,
installPrebuiltRules,
createRuleAssetSavedObject,
} from '../../../utils';
import {
createAlertsIndex,
Expand Down Expand Up @@ -370,6 +373,30 @@ export default ({ getService }: FtrProviderContext) => {
},
]);
});

it('throws an error if rule has external rule source and non-customizable fields are changed', async () => {
// Install base prebuilt detection rule
await createHistoricalPrebuiltRuleAssetSavedObjects(es, [
createRuleAssetSavedObject({ rule_id: 'rule-1', author: ['elastic'] }),
]);
await installPrebuiltRules(es, supertest);

const { body } = await securitySolutionApi
.bulkUpdateRules({
body: [getCustomQueryRuleParams({ rule_id: 'rule-1', author: ['new user'] })],
})
.expect(200);

expect([body[0]]).toEqual([
{
error: {
message: 'Cannot update "author" field for prebuilt rules',
status_code: 400,
},
rule_id: 'rule-1',
},
]);
});
});
});
};
Loading

0 comments on commit 58dc313

Please sign in to comment.