Adding a Rule
September 17, 2026 · View on GitHub
Step-by-step guide using CLDBRN-AWS-EBS-1 as the reference implementation.
Use this guide for both:
- adding a rule to an existing AWS service that already has a dataset
- adding a rule for a new AWS service after its dataset is in place
1. Choose an ID
Use CLDBRN-{PROVIDER}-{SERVICE}-{N} and consult the ID convention before
assigning a number. The reference owns immutable IDs, allocation rules, and retired slots. Review the
compatibility status before removing or reordering a published rule.
2. Decide Whether You Need a Dataset Change
Before writing the rule, check whether the target service already exposes a normalized dataset in packages/rules/src/shared/metadata.ts.
- If the existing dataset already contains the fields your rule needs, reuse it. Only add the rule and tests.
- If the service already exists but the dataset is missing required fields, extend the existing normalized dataset instead of inventing a second overlapping dataset.
- If the rule should behave the same in both
iacanddiscovery, prefer one shared dataset key acrossStaticDatasetMapandDiscoveryDatasetMap, as withaws-s3-bucket-analyses. - If no dataset exists yet, add one first with:
- adding-a-static-dataset.md for IaC
- adding-a-provider-resource.md for discovery
3. Create the Rule File
Place it in packages/rules/src/{provider}/{service}/{kebab-case-name}.ts.
Use the EBS current-generation rule as the executable reference for a dual-mode declaration, matching resource identities, and finding precedence. Its implementation and helpers own the current signatures; adapt the policy to your new rule.
Key patterns:
- Use
createRule()for all built-in rules. - Add a generic rule-level
messagethat works for both discovery and IaC. - Assign
high,medium, orlowseverity from the rule's relative cost impact and pass the same value tocreateFinding(). - For static IaC rules, declare
staticDependenciesdataset keys. - For live AWS rules, declare
discoveryDependenciesdataset keys. - Use
optionalDiscoveryDependenciesonly when the evaluator can improve its decision with a dataset requested by another active rule but can still reach a valid result when that dataset is absent or unavailable. Optional dependencies do not trigger dataset loading on their own. - Use
supersedesRuleIdsonly when this rule's emitted identity is stronger evidence for the same resource and action. Precedence compares normalizedopportunityIdidentity — provider, account, Region, resource namespace, canonical resource ID, andactionType— so the target finding remains unless both rules are active and emit the identical scoped identity and action. Different actions on the same resource are separate opportunities. - When a live finding carries an action, wrap it in
createRecommendationMatch(provider, match, provenance)so it gets normalizedrecommendationprovenance and identity. Provenance timestamps must come from the evidence source; omit them when unknown instead of using evaluation time. See finding-shape.md. - When a finding carries source-reported financial evidence, attach
impactwithcurrentCostandpotentialSavingsbuilt bycreateFinancialEvidence({ amount, currency, period, confidence }). Pass the source values through unchanged — never estimate amounts the source did not provide, and never substitute zero for a missing figure: the helper emitsconfidence: 'unknown'with areasoninstead. Useconfidence: 'estimated'for modeled values and reserve'exact'for measured or billed evidence. See finding-shape.md. - Reuse an existing dataset key when the service already exposes the normalized fields you need.
- If the same policy should work in both scan modes, keep the static and discovery predicates aligned and extract shared helpers when that reduces duplication.
- Read static data from
StaticEvaluationContext.resourceswithresources.get('<dataset-key>'). - Read discovery data from
LiveEvaluationContext.resourceswithresources.get('<dataset-key>'). - Do not declare Terraform type strings, CloudFormation type strings, Resource Explorer
resourceTypes, or loader wiring in rule files. - Return one grouped
Findingornull, never a flatFinding[]. - Keep
ruleId,service,severity,source, andmessageon the parent group. - Put only varying resource-level data on each
FindingMatch. - Omit unavailable
accountIdandregionfields instead of emitting empty strings.
4. Register in the Service Index
Add your rule export to packages/rules/src/aws/{service}/index.ts:
import { ebsVolumeTypeCurrentGenRule } from './volume-type-current-gen.js';
export const ebsRules = [ebsVolumeTypeCurrentGenRule];
If this is a new service, create the index.ts and add the service rules array to the provider index.
5. Register in the Provider Index
Ensure the service array is spread into packages/rules/src/aws/index.ts:
export const awsRules = [...ec2Rules, ...ebsRules, ...rdsRules, ...s3Rules, ...lambdaRules];
6. Preset Inclusion
awsCorePreset in packages/rules/src/presets/aws-core.ts normally includes IDs from awsRules. Rules that require account-wide infrastructure or other explicit setup can be excluded from the preset and enabled by users through enabled-rules. Document any opt-in requirement in rule-ids.md.
The open-source SDK does not own downstream product profiles. Applications can deliberately include a new rule in a
product by adding its public ID to config.discovery.enabledRules. Keep generic rule metadata and normalized resource
evidence in the rule and SDK dataset registry; keep product-specific selection, remediation policy, and presentation in
the consuming application.
7. Update rule-ids.md
Add a row to the Rule Table in rule-ids.md for the new rule:
| `CLDBRN-AWS-EBS-1` | medium | Flags previous-generation EBS volume types ... | ebs | discovery, iac |
Columns: ID, Severity, Description, Service, Supports. The description should explain what the rule flags, including thresholds and skip conditions.
8. Write Tests
All tests live in packages/rules/test/.
exports.test.tsverifies the package export surface remains valid.rule-metadata.test.tsverifies metadata fields are populated.- Add a rule-specific evaluator test file for behavior.
Use the EBS evaluator tests for fixture builders,
LiveResourceBag / StaticResourceBag setup, complete finding assertions, and non-matching cases. Assert group-level
severity and the resource identity fields emitted by the rule, including live resourceType where applicable.
For dual-mode rules on an existing service, add both live and static evaluator coverage unless the rule is intentionally single-mode.
If the live verdict joins a second dataset or reads optional datasets, implement getLiveEvaluationCoverage so
resources without usable evidence report as unknown. The rules metadata test fails otherwise; when the joined dataset
is a complete inventory whose absence is the evidence, add the rule to that test's exemption list with the reason.
For IaC-capable rules, do not stop at one source kind:
- Add evaluator coverage for Terraform-shaped static resources.
- Add evaluator coverage for CloudFormation-shaped static resources.
- Add or extend SDK static dataset/scanner tests when needed so both source kinds are exercised through the loading pipeline.
9. Verify
Run the new evaluator file while iterating, for example:
pnpm --filter @cloudburn/rules exec vitest run test/volume-type-current-gen.test.ts
Then run pnpm verify for metadata, package boundaries, source tests, and installed-package behavior. Follow the
release guide for the changeset required by a user-facing rule addition.
The SDK later groups these rule-level findings under providers in the public ScanResult.