Skip to content

Commit ec078bc

Browse files
feat: add cost and tagging guardrails (#71)
* docs: specify cost guardrails and global tagging * feat: add cost and tagging guardrails * fix(sdk): make global tagging opt-in Keep account-wide tagging available for explicit enablement while preserving the supported local-only Resource Explorer setup for default discovery. * refactor(sdk): share resource explorer plumbing Deduplicate Resource Explorer view resolution, aggregator requirements, paginated resource collection, and discovery initialization finalization. --------- Co-authored-by: Danny Steenman <danny@towardsthecloud.com>
1 parent e66ff1f commit ec078bc

43 files changed

Lines changed: 1119 additions & 144 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cloudburn/rules': minor
3+
---
4+
5+
Add an exceeded-budget rule and an opt-in global untagged-resource rule with their public discovery dataset contracts.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cloudburn/sdk': minor
3+
---
4+
5+
Hydrate normalized AWS Budget spend details and support opt-in global untagged-resource discovery through a Resource Explorer aggregator.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'cloudburn': patch
3+
---
4+
5+
Describe AWS Core defaults and the aggregator requirement for opt-in global tagging discovery in CLI help and starter configuration.

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ npx cloudburn scan ./main.tf
6161

6262
### Config
6363

64-
Config is optional. By default, CloudBurn runs all checks for the mode you use.
64+
Config is optional. By default, CloudBurn runs the AWS Core preset for the mode you use. Account-wide rules such as `CLDBRN-AWS-TAGGING-1` are opt-in through `enabled-rules` because they require an accessible Resource Explorer aggregator.
6565

6666
Create a starter config:
6767

@@ -125,17 +125,19 @@ cloudburn discover init
125125
cloudburn discover
126126
cloudburn discover --region eu-central-1
127127
cloudburn discover --service ec2,s3
128+
cloudburn discover --enabled-rules CLDBRN-AWS-TAGGING-1
128129
cloudburn --debug discover --region eu-central-1
129130
```
130131

131132
The CLI targets one region at a time. Multi-region discovery remains available through the SDK.
133+
`CLDBRN-AWS-TAGGING-1` is opt-in and requires an accessible Resource Explorer aggregator; a local-only setup cannot run account-wide tagging discovery.
132134
Use `--debug` to relay SDK and provider execution trace details to `stderr` while keeping normal command output on `stdout`.
133135

134136
Generate a starter config with `cloudburn config --init`. Full details in the [config reference](docs/reference/config-schema.md).
135137

136138
## AWS Permissions
137139

138-
CloudBurn needs Resource Explorer read/write access plus read-only permissions for the services behind the rules you enable (EC2, EBS, RDS, S3, Lambda, CloudTrail, CloudWatch, etc.). Which permissions you need depends on which rules you're running.
140+
CloudBurn needs Resource Explorer read/write access plus read-only permissions for the services behind the rules you enable (EC2, EBS, RDS, S3, Lambda, CloudTrail, CloudWatch, etc.). `discover init` uses `resource-explorer-2:UpdateView` to enable tag visibility for the opt-in global tagging rule. Which permissions you need depends on which rules you're running.
139141

140142
## Contributing
141143

docs/TESTING.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212

1313
Three test layers, all in `packages/rules/test/`:
1414

15-
| Layer | File | What it verifies |
16-
| ------------------------- | ----------------------- | --------------------------------------------------------------------------------------- |
17-
| **1. Export surface** | `exports.test.ts` | `awsRules` is non-empty, preset rule count matches, `azureRules`/`gcpRules` are empty |
18-
| **2. Metadata contract** | `rule-metadata.test.ts` | Every rule has non-empty `id`, `name`, `description`, and `supports` |
19-
| **3. Evaluator behavior** | `{rule-name}.test.ts` | Full finding payloads for both `evaluateLive` and `evaluateStatic`, plus negative cases |
15+
| Layer | File | What it verifies |
16+
| ------------------------- | ----------------------- | ----------------------------------------------------------------------------------------- |
17+
| **1. Export surface** | `exports.test.ts` | `awsRules` is non-empty, preset inclusion policy holds, `azureRules`/`gcpRules` are empty |
18+
| **2. Metadata contract** | `rule-metadata.test.ts` | Every rule has non-empty `id`, `name`, `description`, and `supports` |
19+
| **3. Evaluator behavior** | `{rule-name}.test.ts` | Full finding payloads for both `evaluateLive` and `evaluateStatic`, plus negative cases |
2020

2121
For static IaC rules, evaluator coverage must include both Terraform-shaped and CloudFormation-shaped resources. A passing test suite for only one source kind is incomplete.
2222

docs/architecture/rules.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ graph LR
7171
Preset --> Export
7272
```
7373

74+
`awsRules` is the complete public AWS rule pack. `awsCorePreset` is the runtime default and may exclude opt-in rules whose infrastructure requirements are not guaranteed by a standard setup.
75+
7476
## Authoring Rules
7577

7678
See [`docs/guides/adding-a-rule.md`](../guides/adding-a-rule.md) for the full end-to-end guide and [`docs/reference/rule-ids.md`](../reference/rule-ids.md) for the ID convention and complete rule table.

docs/architecture/sdk.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,17 @@ Current live-discovery behavior:
7878
- Explicit single-region discovery uses the selected region as the Resource Explorer control plane instead of the ambient current region.
7979
- Explicit multi-region discovery requires an aggregator index and fails fast when one is not enabled.
8080
- Discovery resolves the explicit default Resource Explorer view in the chosen search region and fails if no default view exists or if that default view applies additional filters.
81+
- `discover init` ensures the default Resource Explorer view includes the optional `tags` property. Tag-aware datasets fail with an actionable error when an existing view does not expose tags.
8182
- Discovery setup returns existing local indexes without forcing aggregator creation, and `discover init` retries as local-only setup when cross-region aggregator creation is denied.
8283
- Catalog collection uses Resource Explorer `ListResources` with filter strings instead of `Search`, which avoids the 1,000-result ceiling on filter-only queries.
83-
- Resource Explorer catalog seeding batches `resourcetype:` and `region:` filters into the smallest possible query set, raises `MaxResults` to `1000`, and retries throttled `ListResources` calls before failing.
84+
- Resource Explorer catalog seeding batches `resourcetype:` and `region:` filters into the smallest possible query set, uses `MaxResults: 999` so AWS reliably returns pagination tokens, and retries throttled `ListResources` calls before failing.
8485
- Account-scoped or fallback-backed datasets can bypass Resource Explorer seeding entirely by declaring no `resourceTypes`; the loader then receives `[]` and owns the account-level API call.
8586
- Account-scoped loaders share one lazy STS account-ID resolution per discovery run; the cache is discarded between runs so ambient credential contexts cannot leak identity.
8687
- Each discovery run shares one AWS call budget: concurrent datasets fanning out to the same service in the same region are capped at a combined in-flight limit, on top of each loader's own bounded batches. Route 53 operations additionally share an account-wide sliding-window limit of five request starts per second across hosted zones, record sets, health checks, pagination, retries, and simultaneous discovery runs in the same process. Hydrators called directly outside a discover run stay unbounded.
8788
- `CloudBurnClient.discover({ onProgress })` streams `AwsDiscoveryProgressEvent` values (catalog ready, per-dataset completion counts) while the run executes, so callers can render live feedback without waiting for the final result.
8889
- Resource Explorer catalog failures degrade when the run also requested account-scoped datasets: the SDK records one catalog diagnostic, marks every catalog-backed dataset unavailable (skipping the rules that need them), and still evaluates account-scoped datasets. When every requested dataset needs the catalog, the failure stays fatal so the actionable Resource Explorer error reaches the user.
8990
- Dataset loader failures are non-fatal: the SDK records diagnostics, marks the affected datasets unavailable, skips rules that require them, and returns findings from datasets that loaded successfully.
91+
- Global tagging discovery is opt-in because it requires an accessible aggregator, and uses one paginated `ListResources` filter query (`resourcetype.supports:tags tag:none`) instead of per-service tagging APIs.
9092
- Missing Lambda `Architectures` values from AWS are normalized to `['x86_64']`, matching the AWS default architecture.
9193
- Lambda hydrators limit in-flight `GetFunctionConfiguration` calls per region to avoid API throttling in large accounts.
9294
- Live scans require Resource Explorer access plus narrow hydrator permissions such as `apigateway:GetStage`, `application-autoscaling:DescribeScalableTargets`, `application-autoscaling:DescribeScalingPolicies`, `ce:GetCostAndUsage`, `cloudfront:GetDistribution`, `cloudfront:ListDistributions`, `cloudtrail:DescribeTrails`, `cloudwatch:GetMetricData`, `dynamodb:DescribeTable`, `ecs:DescribeContainerInstances`, `ecs:DescribeServices`, `ec2:DescribeInstances`, `ec2:DescribeNatGateways`, `ec2:DescribeVolumes`, `eks:ListNodegroups`, `eks:DescribeNodegroup`, `lambda:GetFunctionConfiguration`, `rds:DescribeDBInstances`, `route53:ListHealthChecks`, `route53:ListHostedZones`, `route53:ListResourceRecordSets`, `s3:GetLifecycleConfiguration`, `s3:GetIntelligentTieringConfiguration`, `sagemaker:DescribeEndpoint`, `sagemaker:DescribeEndpointConfig`, `sagemaker:DescribeNotebookInstance`, and `secretsmanager:DescribeSecret`.
@@ -123,7 +125,7 @@ absolute filesystem paths.
123125

124126
## Provider Layer
125127

126-
`buildRuleRegistry(config, mode)` decides which rules are active for the requested mode.
128+
`buildRuleRegistry(config, mode)` decides which rules are active for the requested mode. When `enabledRules` is unset, it starts from `awsCorePreset`; an explicit `enabledRules` list replaces that selection and can activate opt-in rules from the complete `awsRules` export.
127129

128130
Config behavior: see [`docs/reference/config-schema.md`](../reference/config-schema.md) for full field definitions, merge behavior, and config loading semantics. Registry filtering is mode-aware and only activates rules that support the requested source.
129131

docs/guides/adding-a-rule.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export const awsRules = [...ec2Rules, ...ebsRules, ...rdsRules, ...s3Rules, ...l
102102

103103
## 6. Preset Inclusion
104104

105-
`awsCorePreset` in `packages/rules/src/presets/aws-core.ts` uses `toRuleIds(awsRules)`, so new rules are automatically included when added to `awsRules`. No manual preset change is needed.
105+
`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`](../reference/rule-ids.md).
106106

107107
## 7. Update rule-ids.md
108108

docs/reference/config-schema.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ Source of truth: `packages/sdk/src/types.ts` (type), `packages/sdk/src/config/de
1111

1212
Each mode uses the same fields:
1313

14-
| Field | Type | Default | Description |
15-
| ---------------- | ------------------- | ------- | ------------------------------------------------------------------------ |
16-
| `enabled-rules` | `string[]` | unset | If present, only the listed rule IDs remain active for that mode. |
17-
| `disabled-rules` | `string[]` | unset | Rule IDs to remove from the active set after `enabled-rules` is applied. |
18-
| `services` | `string[]` | unset | Service allowlist applied before `enabled-rules` and `disabled-rules`. |
19-
| `format` | `'json' \| 'table'` | unset | Default CLI output format for that mode when `--format` is not passed. |
14+
| Field | Type | Default | Description |
15+
| ---------------- | ------------------- | ------- | ------------------------------------------------------------------------------- |
16+
| `enabled-rules` | `string[]` | unset | If present, only the listed rule IDs remain active; otherwise AWS Core is used. |
17+
| `disabled-rules` | `string[]` | unset | Rule IDs to remove from the active set after `enabled-rules` is applied. |
18+
| `services` | `string[]` | unset | Service allowlist applied before `enabled-rules` and `disabled-rules`. |
19+
| `format` | `'json' \| 'table'` | unset | Default CLI output format for that mode when `--format` is not passed. |
2020

2121
## Merge Behavior
2222

@@ -58,8 +58,9 @@ Printed by `cloudburn config --print-template` (from `packages/cloudburn/src/com
5858

5959
```yaml
6060
# Static IaC scan configuration.
61-
# enabled-rules restricts scans to only the listed rule IDs.
62-
# disabled-rules removes specific rule IDs from the active set.
61+
# enabled-rules replaces the AWS Core preset with only the listed rule IDs.
62+
# It can also activate opt-in rules that are not in AWS Core.
63+
# disabled-rules removes specific rule IDs from the selected set.
6364
# services restricts scans to rules for the listed services.
6465
# format sets the default output format when --format is not passed.
6566
iac:

docs/reference/rule-ids.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`
2626
| `CLDBRN-AWS-CLOUDWATCH-3` | Reviews only log groups storing at least `1 GiB` and flags them when no metric filters are configured. | cloudwatch | discovery |
2727
| `CLDBRN-AWS-COSTGUARDRAILS-1` | Flags accounts whose AWS Budgets summary reports zero configured budgets. | costguardrails | discovery |
2828
| `CLDBRN-AWS-COSTGUARDRAILS-2` | Flags accounts whose Cost Anomaly Detection summary reports zero anomaly monitors. | costguardrails | discovery |
29+
| `CLDBRN-AWS-COSTGUARDRAILS-3` | Flags configured AWS Budgets only when normalized actual spend is strictly greater than the same-unit budget limit. Malformed and unit-mismatched spend details are skipped. | costguardrails | discovery |
2930
| `CLDBRN-AWS-COSTEXPLORER-1` | Compares the last two full months and flags only services with an existing prior-month baseline and a cost increase greater than `10` cost units. | costexplorer | discovery |
3031
| `CLDBRN-AWS-DYNAMODB-1` | Flags only tables whose parsed `latestStreamLabel` is older than `90` days. Tables without a stream label are skipped. | dynamodb | discovery |
3132
| `CLDBRN-AWS-DYNAMODB-2` | Reviews only provisioned-capacity tables and flags them when no table-level read or write autoscaling targets are configured. | dynamodb | discovery, iac |
@@ -91,6 +92,7 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`
9192
| `CLDBRN-AWS-SAGEMAKER-1` | Flags only notebook instances whose normalized status remains `InService`. | sagemaker | discovery |
9293
| `CLDBRN-AWS-SAGEMAKER-2` | Flags only endpoints whose normalized status remains `InService`, whose parsed `creationTime` is at least `14` days old, and whose complete 14-day `Invocations` total stays at `0`. Endpoints with incomplete metrics are skipped. | sagemaker | discovery |
9394
| `CLDBRN-AWS-SECRETSMANAGER-1` | Flags secrets with no `lastAccessedDate` and secrets whose parsed last access is at least `90` days old. | secretsmanager | discovery |
95+
| `CLDBRN-AWS-TAGGING-1` | Opt-in. Uses an accessible Resource Explorer aggregator and `resourcetype.supports:tags tag:none` to flag taggable resources without user-created tags across the account. AWS-managed tags do not satisfy the rule. | tagging | discovery |
9496
| `CLDBRN-AWS-LAMBDA-1` | Recommend arm64 architecture when compatible. | lambda | iac, discovery |
9597
| `CLDBRN-AWS-LAMBDA-2` | Uses 7-day CloudWatch totals and flags only functions whose observed `Errors / Invocations` ratio is greater than `10%`. | lambda | discovery |
9698
| `CLDBRN-AWS-LAMBDA-3` | Reviews only functions with configured timeouts of at least `30` seconds and flags when the timeout is at least `5x` the observed 7-day average duration. | lambda | discovery |
@@ -99,8 +101,8 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`
99101

100102
## Presets
101103

102-
| Preset ID | Name | Rule IDs |
103-
| ---------- | -------- | ------------------- |
104-
| `aws-core` | AWS Core | All AWS rules above |
104+
| Preset ID | Name | Rule IDs |
105+
| ---------- | -------- | -------------------------------------------------------- |
106+
| `aws-core` | AWS Core | All AWS rules above except opt-in `CLDBRN-AWS-TAGGING-1` |
105107

106108
Future presets (planned): `strict`, `startup`, `production`.

0 commit comments

Comments
 (0)