Skip to content

Commit f44751b

Browse files
authored
feat(rules): add AWS discovery rules for edge and cost services (#47)
1 parent 9413ce1 commit f44751b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+3919
-30
lines changed
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+
Add AWS discovery loaders and registry wiring for API Gateway stages, CloudFront distributions, Cost Explorer service spend deltas, DynamoDB tables and autoscaling, Route 53 zones, records, and health checks, and Secrets Manager secrets.

.changeset/clear-rule-harvest.md

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 AWS discovery rules for API Gateway stage caching, CloudFront price class review, Cost Explorer month-over-month increases, DynamoDB stale tables and autoscaling coverage, Route 53 TTL and unused health checks, and unused Secrets Manager secrets.

docs/architecture/sdk.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,11 @@ Current live-discovery behavior:
7777
- 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.
7878
- 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.
7979
- Catalog collection uses Resource Explorer `ListResources` with filter strings instead of `Search`, which avoids the 1,000-result ceiling on filter-only queries.
80+
- 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.
8081
- Resource Explorer inventory failures and dataset loader failures are fatal. The SDK does not degrade to partial live results.
8182
- Missing Lambda `Architectures` values from AWS are normalized to `['x86_64']`, matching the AWS default architecture.
8283
- Lambda hydrators limit in-flight `GetFunctionConfiguration` calls per region to avoid API throttling in large accounts.
83-
- Live scans require Resource Explorer access plus narrow hydrator permissions such as `application-autoscaling:DescribeScalableTargets`, `application-autoscaling:DescribeScalingPolicies`, `cloudtrail:DescribeTrails`, `cloudwatch:GetMetricData`, `ecs:DescribeContainerInstances`, `ecs:DescribeServices`, `ec2:DescribeVolumes`, `ec2:DescribeInstances`, `eks:ListNodegroups`, `eks:DescribeNodegroup`, `lambda:GetFunctionConfiguration`, `rds:DescribeDBInstances`, `s3:GetLifecycleConfiguration`, and `s3:GetIntelligentTieringConfiguration`.
84+
- 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:DescribeVolumes`, `ec2:DescribeInstances`, `eks:ListNodegroups`, `eks:DescribeNodegroup`, `lambda:GetFunctionConfiguration`, `rds:DescribeDBInstances`, `route53:ListHealthChecks`, `route53:ListHostedZones`, `route53:ListResourceRecordSets`, `s3:GetLifecycleConfiguration`, `s3:GetIntelligentTieringConfiguration`, and `secretsmanager:DescribeSecret`.
8485

8586
## Public Result Shape
8687

docs/reference/rule-ids.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`
1616

1717
| ID | Name | Service | Supports | Status |
1818
| --------------------- | ----------------------------------------- | ------- | -------------- | ----------- |
19+
| `CLDBRN-AWS-APIGATEWAY-1` | API Gateway Stage Caching Disabled | apigateway | discovery | Implemented |
20+
| `CLDBRN-AWS-CLOUDFRONT-1` | CloudFront Distribution Price Class All | cloudfront | discovery | Implemented |
1921
| `CLDBRN-AWS-CLOUDTRAIL-1` | CloudTrail Redundant Global Trails | cloudtrail | discovery | Implemented |
2022
| `CLDBRN-AWS-CLOUDTRAIL-2` | CloudTrail Redundant Regional Trails | cloudtrail | discovery | Implemented |
2123
| `CLDBRN-AWS-CLOUDWATCH-1` | CloudWatch Log Group Missing Retention | cloudwatch | discovery | Implemented |
2224
| `CLDBRN-AWS-CLOUDWATCH-2` | CloudWatch Unused Log Streams | cloudwatch | discovery | Implemented |
25+
| `CLDBRN-AWS-COSTEXPLORER-1` | Cost Explorer Full Month Cost Changes | costexplorer | discovery | Implemented |
26+
| `CLDBRN-AWS-DYNAMODB-1` | DynamoDB Table Stale Data | dynamodb | discovery | Implemented |
27+
| `CLDBRN-AWS-DYNAMODB-2` | DynamoDB Table Without Autoscaling | dynamodb | discovery | Implemented |
2328
| `CLDBRN-AWS-EC2-1` | EC2 Instance Type Not Preferred | ec2 | iac, discovery | Implemented |
2429
| `CLDBRN-AWS-EC2-2` | S3 Interface VPC Endpoint Used | ec2 | iac | Implemented |
2530
| `CLDBRN-AWS-EC2-3` | Elastic IP Address Unassociated | ec2 | discovery | Implemented |
@@ -58,12 +63,19 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`
5863
| `CLDBRN-AWS-REDSHIFT-1` | Redshift Cluster Low CPU Utilization | redshift | discovery | Implemented |
5964
| `CLDBRN-AWS-REDSHIFT-2` | Redshift Cluster Missing Reserved Coverage | redshift | discovery | Implemented |
6065
| `CLDBRN-AWS-REDSHIFT-3` | Redshift Cluster Pause Resume Not Enabled | redshift | discovery | Implemented |
66+
| `CLDBRN-AWS-ROUTE53-1` | Route 53 Record Higher TTL | route53 | discovery | Implemented |
67+
| `CLDBRN-AWS-ROUTE53-2` | Route 53 Health Check Unused | route53 | discovery | Implemented |
6168
| `CLDBRN-AWS-S3-1` | S3 Missing Lifecycle Configuration | s3 | iac, discovery | Implemented |
6269
| `CLDBRN-AWS-S3-2` | S3 Bucket Storage Class Not Optimized | s3 | iac, discovery | Implemented |
70+
| `CLDBRN-AWS-SECRETSMANAGER-1` | Secrets Manager Secret Unused | secretsmanager | discovery | Implemented |
6371
| `CLDBRN-AWS-LAMBDA-1` | Lambda Cost Optimal Architecture | lambda | iac, discovery | Implemented |
6472
| `CLDBRN-AWS-LAMBDA-2` | Lambda Function High Error Rate | lambda | discovery | Implemented |
6573
| `CLDBRN-AWS-LAMBDA-3` | Lambda Function Excessive Timeout | lambda | discovery | Implemented |
6674

75+
`CLDBRN-AWS-APIGATEWAY-1` flags REST API stages when `cacheClusterEnabled` is not explicitly `true`.
76+
77+
`CLDBRN-AWS-CLOUDFRONT-1` reviews only distributions using `PriceClass_All`.
78+
6779
`CLDBRN-AWS-EBS-1` flags previous-generation EBS volume types (`gp2`, `io1`, and `standard`) and does not flag current-generation HDD families such as `st1` or `sc1`.
6880

6981
`CLDBRN-AWS-EBS-4` treats volumes above `100 GiB` as oversized enough to warrant explicit review.
@@ -76,6 +88,12 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`
7688

7789
`CLDBRN-AWS-CLOUDWATCH-2` flags log streams with no observed event history and log streams whose `lastIngestionTime` is more than 90 days old. Delivery-managed log groups remain exempt.
7890

91+
`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.
92+
93+
`CLDBRN-AWS-DYNAMODB-1` flags only tables whose parsed `latestStreamLabel` is older than `90` days. Tables without a stream label are skipped.
94+
95+
`CLDBRN-AWS-DYNAMODB-2` reviews only provisioned-capacity tables and flags them when no table-level read or write autoscaling targets are configured.
96+
7997
`CLDBRN-AWS-EC2-6` flags only families with a curated Graviton-equivalent path. Instances without architecture metadata or outside the curated family set are skipped.
8098

8199
`CLDBRN-AWS-EC2-7` reviews only active reserved instances with an `endTime` inside the next 60 days.
@@ -120,6 +138,12 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`
120138

121139
`CLDBRN-AWS-REDSHIFT-3` flags only `available`, VPC-backed clusters with automated snapshots enabled, no HSM, and no Multi-AZ deployment when either the pause or resume schedule is missing.
122140

141+
`CLDBRN-AWS-ROUTE53-1` reviews only non-alias records and treats `3600` seconds as the low-TTL floor.
142+
143+
`CLDBRN-AWS-ROUTE53-2` flags only Route 53 health checks that are not referenced by any discovered record set.
144+
145+
`CLDBRN-AWS-SECRETSMANAGER-1` flags secrets with no `lastAccessedDate` and secrets whose parsed last access is at least `90` days old.
146+
123147
**Status key:**
124148

125149
- **Implemented** — has evaluator coverage for every scan mode listed in `supports`
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';
2+
3+
const RULE_ID = 'CLDBRN-AWS-APIGATEWAY-1';
4+
const RULE_SERVICE = 'apigateway';
5+
const RULE_MESSAGE = 'API Gateway REST API stages should enable caching when stage caching is available.';
6+
7+
/** Flag API Gateway REST API stages whose cache cluster is disabled. */
8+
export const apiGatewayCachingDisabledRule = createRule({
9+
id: RULE_ID,
10+
name: 'API Gateway Stage Caching Disabled',
11+
description: 'Flag API Gateway REST API stages with caching disabled.',
12+
message: RULE_MESSAGE,
13+
provider: 'aws',
14+
service: RULE_SERVICE,
15+
supports: ['discovery'],
16+
discoveryDependencies: ['aws-apigateway-stages'],
17+
evaluateLive: ({ resources }) => {
18+
const findings = resources
19+
.get('aws-apigateway-stages')
20+
.filter((stage) => stage.cacheClusterEnabled !== true)
21+
.map((stage) => createFindingMatch(stage.stageArn, stage.region, stage.accountId));
22+
23+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
24+
},
25+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { apiGatewayCachingDisabledRule } from './caching-disabled.js';
2+
3+
// Intent: aggregate AWS API Gateway rule definitions.
4+
export const apigatewayRules = [apiGatewayCachingDisabledRule];
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';
2+
3+
const RULE_ID = 'CLDBRN-AWS-CLOUDFRONT-1';
4+
const RULE_SERVICE = 'cloudfront';
5+
const RULE_MESSAGE = 'CloudFront distributions using PriceClass_All should be reviewed for cheaper edge coverage.';
6+
7+
/** Flag CloudFront distributions that use the most expensive global price class. */
8+
export const cloudFrontDistributionPricingClassRule = createRule({
9+
id: RULE_ID,
10+
name: 'CloudFront Distribution Price Class All',
11+
description: 'Flag CloudFront distributions using PriceClass_All when a cheaper price class may suffice.',
12+
message: RULE_MESSAGE,
13+
provider: 'aws',
14+
service: RULE_SERVICE,
15+
supports: ['discovery'],
16+
discoveryDependencies: ['aws-cloudfront-distributions'],
17+
evaluateLive: ({ resources }) => {
18+
const findings = resources
19+
.get('aws-cloudfront-distributions')
20+
.filter((distribution) => distribution.priceClass === 'PriceClass_All')
21+
.map((distribution) =>
22+
createFindingMatch(distribution.distributionArn, distribution.region, distribution.accountId),
23+
);
24+
25+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
26+
},
27+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { cloudFrontDistributionPricingClassRule } from './distribution-pricing-class.js';
2+
3+
// Intent: aggregate AWS CloudFront rule definitions.
4+
export const cloudfrontRules = [cloudFrontDistributionPricingClassRule];
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';
2+
3+
const RULE_ID = 'CLDBRN-AWS-COSTEXPLORER-1';
4+
const RULE_SERVICE = 'costexplorer';
5+
const RULE_MESSAGE =
6+
'AWS services with cost increases greater than 10 USD between the last two full months should be reviewed.';
7+
// Match the upstream Thrifty default and only flag material month-over-month increases above ten cost units.
8+
const COST_INCREASE_THRESHOLD = 10;
9+
10+
/** Flag AWS services whose spend increased materially between the last two full months. */
11+
export const costExplorerFullMonthCostChangesRule = createRule({
12+
id: RULE_ID,
13+
name: 'Cost Explorer Full Month Cost Changes',
14+
description: 'Flag services with significant cost increases between the last two full months.',
15+
message: RULE_MESSAGE,
16+
provider: 'aws',
17+
service: RULE_SERVICE,
18+
supports: ['discovery'],
19+
discoveryDependencies: ['aws-cost-usage'],
20+
evaluateLive: ({ resources }) => {
21+
const findings = resources
22+
.get('aws-cost-usage')
23+
.filter((service) => service.previousMonthCost > 0 && service.costIncrease > COST_INCREASE_THRESHOLD)
24+
.map((service) => createFindingMatch(`cost/${service.serviceSlug}`, undefined, service.accountId));
25+
26+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
27+
},
28+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { costExplorerFullMonthCostChangesRule } from './full-month-cost-changes.js';
2+
3+
// Intent: aggregate AWS Cost Explorer rule definitions.
4+
export const costexplorerRules = [costExplorerFullMonthCostChangesRule];

0 commit comments

Comments
 (0)