[8.x] [Security Solution] Prevent non-customizable fields from updating 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)](https://github.com/elastic/kibana/pull/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":"56367316+dplumlee@users.noreply.github.com"},"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
<elasticmachine@users.noreply.github.com>","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
<elasticmachine@users.noreply.github.com>","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
<elasticmachine@users.noreply.github.com>","sha":"00042177a8e976d379b5e40db3664db1e333999d"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Davis Plumlee <56367316+dplumlee@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2024-10-11 11:36:22 +11:00 committed by GitHub
parent 3521b8c330
commit 58dc313abb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 218 additions and 13 deletions

View file

@ -18,6 +18,7 @@ export const getPrebuiltRuleMock = (rewrites?: Partial<PrebuiltRuleAsset>): Preb
language: 'kuery',
rule_id: 'rule-1',
version: 1,
author: [],
...rewrites,
} as PrebuiltRuleAsset);
@ -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: [

View file

@ -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

View file

@ -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'
);
});
});
});

View file

@ -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';
@ -51,6 +52,8 @@ export const patchRule = async ({
await validateMlAuth(mlAuthz, rulePatch.type ?? existingRule.type);
validateNonCustomizablePatchFields(rulePatch, existingRule);
const patchedRule = await applyRulePatch({
prebuiltRuleAssetClient,
existingRule,

View file

@ -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';
@ -50,6 +51,8 @@ export const updateRule = async ({
throw new ClientError(error.message, error.statusCode);
}
validateNonCustomizableUpdateFields(ruleUpdate, existingRule);
const ruleWithUpdates = await applyRuleUpdate({
prebuiltRuleAssetClient,
existingRule,

View file

@ -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,
@ -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,
@ -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);
}
}
};

View file

@ -16,6 +16,9 @@ import {
removeServerGeneratedPropertiesIncludingRuleId,
getSimpleRuleOutputWithoutRuleId,
updateUsername,
createHistoricalPrebuiltRuleAssetSavedObjects,
installPrebuiltRules,
createRuleAssetSavedObject,
} from '../../../utils';
import {
createAlertsIndex,
@ -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);

View file

@ -16,6 +16,9 @@ import {
getSimpleRuleOutputWithoutRuleId,
removeServerGeneratedPropertiesIncludingRuleId,
updateUsername,
createHistoricalPrebuiltRuleAssetSavedObjects,
installPrebuiltRules,
createRuleAssetSavedObject,
} from '../../../utils';
import {
createAlertsIndex,
@ -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',
},
]);
});
});
});
};

View file

@ -18,6 +18,9 @@ import {
getSimpleMlRuleUpdate,
getSimpleRule,
updateUsername,
createHistoricalPrebuiltRuleAssetSavedObjects,
installPrebuiltRules,
createRuleAssetSavedObject,
} from '../../../utils';
import {
createAlertsIndex,
@ -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');
});
});
});
};

View file

@ -17,6 +17,9 @@ import {
getSimpleRuleUpdate,
getSimpleRule,
updateUsername,
createHistoricalPrebuiltRuleAssetSavedObjects,
installPrebuiltRules,
createRuleAssetSavedObject,
} from '../../../utils';
import {
createAlertsIndex,
@ -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',
},
]);
});
});
});
};

View file

@ -31,6 +31,7 @@ import {
getRuleSavedObjectWithLegacyInvestigationFields,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray,
createRuleThroughAlertingEndpoint,
getCustomQueryRuleParams,
} from '../../../utils';
import {
createRule,
@ -1140,7 +1141,7 @@ export default ({ getService }: FtrProviderContext) => {
await installMockPrebuiltRules(supertest, es);
const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID });
const hookAction = await createWebHookRuleAction(supertest);
const newRuleToUpdate = getSimpleRule(immutableRule.rule_id);
const newRuleToUpdate = getCustomQueryRuleParams({ rule_id: immutableRule.rule_id });
const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, false, newRuleToUpdate);
await updateRule(supertest, ruleToUpdate);
@ -1161,7 +1162,7 @@ export default ({ getService }: FtrProviderContext) => {
...omittedFields
} = foundRule;
expect(omittedFields).to.eql({
rule_name: 'Simple Rule Query',
rule_name: 'Custom query rule',
rule_type: 'query',
enabled: false,
elastic_rule: true,
@ -1197,7 +1198,7 @@ export default ({ getService }: FtrProviderContext) => {
await installMockPrebuiltRules(supertest, es);
const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID });
const hookAction = await createWebHookRuleAction(supertest);
const newRuleToUpdate = getSimpleRule(immutableRule.rule_id);
const newRuleToUpdate = getCustomQueryRuleParams({ rule_id: immutableRule.rule_id });
const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, true, newRuleToUpdate);
await updateRule(supertest, ruleToUpdate);
@ -1218,7 +1219,7 @@ export default ({ getService }: FtrProviderContext) => {
...omittedFields
} = foundRule;
expect(omittedFields).to.eql({
rule_name: 'Simple Rule Query',
rule_name: 'Custom query rule',
rule_type: 'query',
enabled: true,
elastic_rule: true,
@ -1254,7 +1255,7 @@ export default ({ getService }: FtrProviderContext) => {
await installMockPrebuiltRules(supertest, es);
const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID });
const hookAction = await createWebHookRuleAction(supertest);
const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, false);
const newRuleToUpdate = getCustomQueryRuleParams({ rule_id: immutableRule.rule_id });
await updateRule(supertest, newRuleToUpdate);
await createLegacyRuleAction(supertest, immutableRule.id, hookAction.id);
@ -1275,7 +1276,7 @@ export default ({ getService }: FtrProviderContext) => {
...omittedFields
} = foundRule;
expect(omittedFields).to.eql({
rule_name: 'Simple Rule Query',
rule_name: 'Custom query rule',
rule_type: 'query',
enabled: false,
elastic_rule: true,
@ -1311,7 +1312,10 @@ export default ({ getService }: FtrProviderContext) => {
await installMockPrebuiltRules(supertest, es);
const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID });
const hookAction = await createWebHookRuleAction(supertest);
const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, true);
const newRuleToUpdate = getCustomQueryRuleParams({
rule_id: immutableRule.rule_id,
enabled: true,
});
await updateRule(supertest, newRuleToUpdate);
await createLegacyRuleAction(supertest, immutableRule.id, hookAction.id);
@ -1332,7 +1336,7 @@ export default ({ getService }: FtrProviderContext) => {
...omittedFields
} = foundRule;
expect(omittedFields).to.eql({
rule_name: 'Simple Rule Query',
rule_name: 'Custom query rule',
rule_type: 'query',
enabled: true,
elastic_rule: true,

View file

@ -21,13 +21,13 @@ import {
fetchRule,
getRuleWithWebHookAction,
getSimpleMlRule,
getSimpleRule,
getSimpleThreatMatch,
getStats,
getThresholdRuleForAlertTesting,
installMockPrebuiltRules,
updateRule,
deleteAllEventLogExecutionEvents,
getCustomQueryRuleParams,
} from '../../../utils';
import {
createRule,
@ -408,7 +408,7 @@ export default ({ getService }: FtrProviderContext) => {
await installMockPrebuiltRules(supertest, es);
const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID });
const hookAction = await createWebHookRuleAction(supertest);
const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, false);
const newRuleToUpdate = getCustomQueryRuleParams({ rule_id: immutableRule.rule_id });
await updateRule(supertest, newRuleToUpdate);
await createLegacyRuleAction(supertest, immutableRule.id, hookAction.id);
@ -429,7 +429,7 @@ export default ({ getService }: FtrProviderContext) => {
...omittedFields
} = foundRule;
expect(omittedFields).to.eql({
rule_name: 'Simple Rule Query',
rule_name: 'Custom query rule',
rule_type: 'query',
enabled: false,
elastic_rule: true,
@ -465,7 +465,10 @@ export default ({ getService }: FtrProviderContext) => {
await installMockPrebuiltRules(supertest, es);
const immutableRule = await fetchRule(supertest, { ruleId: ELASTIC_SECURITY_RULE_ID });
const hookAction = await createWebHookRuleAction(supertest);
const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, true);
const newRuleToUpdate = getCustomQueryRuleParams({
rule_id: immutableRule.rule_id,
enabled: true,
});
await updateRule(supertest, newRuleToUpdate);
await createLegacyRuleAction(supertest, immutableRule.id, hookAction.id);
@ -486,7 +489,7 @@ export default ({ getService }: FtrProviderContext) => {
...omittedFields
} = foundRule;
expect(omittedFields).to.eql({
rule_name: 'Simple Rule Query',
rule_name: 'Custom query rule',
rule_type: 'query',
enabled: true,
elastic_rule: true,

View file

@ -29,6 +29,7 @@ export function getCustomQueryRuleParams(
index: ['logs-*'],
interval: '100m',
from: 'now-6m',
author: [],
enabled: false,
...rewrites,
};