Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/swift-parrots-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudburn/rules": minor
---

Add IaC evaluation support for high-confidence AWS rules covering EBS sizing and IOPS checks, EC2 instance and Elastic IP reviews, RDS Graviton and engine-version checks, API Gateway stages, CloudFront price classes, CloudWatch log retention, DynamoDB autoscaling, EKS node groups, EMR instance generations, and Route 53 TTL and health-check usage.
5 changes: 5 additions & 0 deletions .changeset/tall-comics-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudburn/sdk": minor
---

Add static AWS dataset loaders for the new dual-mode IaC rules, including DynamoDB autoscaling state, Elastic IP association state, EKS node groups, EMR cluster instance types, and Route 53 records and health checks across Terraform and CloudFormation inputs.
34 changes: 17 additions & 17 deletions docs/reference/rule-ids.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,62 +16,62 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`

| ID | Name | Service | Supports | Status |
| --------------------- | ----------------------------------------- | ------- | -------------- | ----------- |
| `CLDBRN-AWS-APIGATEWAY-1` | API Gateway Stage Caching Disabled | apigateway | discovery | Implemented |
| `CLDBRN-AWS-CLOUDFRONT-1` | CloudFront Distribution Price Class All | cloudfront | discovery | Implemented |
| `CLDBRN-AWS-APIGATEWAY-1` | API Gateway Stage Caching Disabled | apigateway | discovery, iac | Implemented |
| `CLDBRN-AWS-CLOUDFRONT-1` | CloudFront Distribution Price Class All | cloudfront | discovery, iac | Implemented |
| `CLDBRN-AWS-CLOUDFRONT-2` | CloudFront Distribution Unused | cloudfront | discovery | Implemented |
| `CLDBRN-AWS-CLOUDTRAIL-1` | CloudTrail Redundant Global Trails | cloudtrail | discovery | Implemented |
| `CLDBRN-AWS-CLOUDTRAIL-2` | CloudTrail Redundant Regional Trails | cloudtrail | discovery | Implemented |
| `CLDBRN-AWS-CLOUDWATCH-1` | CloudWatch Log Group Missing Retention | cloudwatch | discovery | Implemented |
| `CLDBRN-AWS-CLOUDWATCH-1` | CloudWatch Log Group Missing Retention | cloudwatch | discovery, iac | Implemented |
| `CLDBRN-AWS-CLOUDWATCH-2` | CloudWatch Unused Log Streams | cloudwatch | discovery | Implemented |
| `CLDBRN-AWS-CLOUDWATCH-3` | CloudWatch Log Group No Metric Filters | cloudwatch | discovery | Implemented |
| `CLDBRN-AWS-COSTGUARDRAILS-1` | AWS Budgets Missing | costguardrails | discovery | Implemented |
| `CLDBRN-AWS-COSTGUARDRAILS-2` | Cost Anomaly Detection Missing | costguardrails | discovery | Implemented |
| `CLDBRN-AWS-COSTEXPLORER-1` | Cost Explorer Full Month Cost Changes | costexplorer | discovery | Implemented |
| `CLDBRN-AWS-DYNAMODB-1` | DynamoDB Table Stale Data | dynamodb | discovery | Implemented |
| `CLDBRN-AWS-DYNAMODB-2` | DynamoDB Table Without Autoscaling | dynamodb | discovery | Implemented |
| `CLDBRN-AWS-DYNAMODB-2` | DynamoDB Table Without Autoscaling | dynamodb | discovery, iac | Implemented |
| `CLDBRN-AWS-DYNAMODB-3` | DynamoDB Table Unused | dynamodb | discovery | Implemented |
| `CLDBRN-AWS-EC2-1` | EC2 Instance Type Not Preferred | ec2 | iac, discovery | Implemented |
| `CLDBRN-AWS-EC2-2` | S3 Interface VPC Endpoint Used | ec2 | iac | Implemented |
| `CLDBRN-AWS-EC2-3` | Elastic IP Address Unassociated | ec2 | discovery | Implemented |
| `CLDBRN-AWS-EC2-3` | Elastic IP Address Unassociated | ec2 | discovery, iac | Implemented |
| `CLDBRN-AWS-EC2-4` | VPC Interface Endpoint Inactive | ec2 | discovery | Implemented |
| `CLDBRN-AWS-EC2-5` | EC2 Instance Low Utilization | ec2 | discovery | Implemented |
| `CLDBRN-AWS-EC2-6` | EC2 Instance Without Graviton | ec2 | discovery | Implemented |
| `CLDBRN-AWS-EC2-6` | EC2 Instance Without Graviton | ec2 | discovery, iac | Implemented |
| `CLDBRN-AWS-EC2-7` | EC2 Reserved Instance Expiring | ec2 | discovery | Implemented |
| `CLDBRN-AWS-EC2-8` | EC2 Instance Large Size | ec2 | discovery | Implemented |
| `CLDBRN-AWS-EC2-8` | EC2 Instance Large Size | ec2 | discovery, iac | Implemented |
| `CLDBRN-AWS-EC2-9` | EC2 Instance Long Running | ec2 | discovery | Implemented |
| `CLDBRN-AWS-ECS-1` | ECS Container Instance Without Graviton | ecs | discovery | Implemented |
| `CLDBRN-AWS-ECS-2` | ECS Cluster Low CPU Utilization | ecs | discovery | Implemented |
| `CLDBRN-AWS-ECS-3` | ECS Service Missing Autoscaling Policy | ecs | discovery | Implemented |
| `CLDBRN-AWS-EBS-1` | EBS Volume Type Not Current Generation | ebs | discovery, iac | Implemented |
| `CLDBRN-AWS-EBS-2` | EBS Volume Unattached | ebs | discovery | Implemented |
| `CLDBRN-AWS-EBS-3` | EBS Volume Attached To Stopped Instances | ebs | discovery | Implemented |
| `CLDBRN-AWS-EBS-4` | EBS Volume Large Size | ebs | discovery | Implemented |
| `CLDBRN-AWS-EBS-5` | EBS Volume High Provisioned IOPS | ebs | discovery | Implemented |
| `CLDBRN-AWS-EBS-6` | EBS Volume Low Provisioned IOPS On io1/io2 | ebs | discovery | Implemented |
| `CLDBRN-AWS-EBS-4` | EBS Volume Large Size | ebs | discovery, iac | Implemented |
| `CLDBRN-AWS-EBS-5` | EBS Volume High Provisioned IOPS | ebs | discovery, iac | Implemented |
| `CLDBRN-AWS-EBS-6` | EBS Volume Low Provisioned IOPS On io1/io2 | ebs | discovery, iac | Implemented |
| `CLDBRN-AWS-EBS-7` | EBS Snapshot Max Age Exceeded | ebs | discovery | Implemented |
| `CLDBRN-AWS-ECR-1` | ECR Repository Missing Lifecycle Policy | ecr | iac, discovery | Implemented |
| `CLDBRN-AWS-EKS-1` | EKS Node Group Without Graviton | eks | discovery | Implemented |
| `CLDBRN-AWS-EKS-1` | EKS Node Group Without Graviton | eks | discovery, iac | Implemented |
| `CLDBRN-AWS-ELASTICACHE-1` | ElastiCache Cluster Missing Reserved Coverage | elasticache | discovery | Implemented |
| `CLDBRN-AWS-ELASTICACHE-2` | ElastiCache Cluster Idle | elasticache | discovery | Implemented |
| `CLDBRN-AWS-ELB-1` | Application Load Balancer Without Targets | elb | discovery | Implemented |
| `CLDBRN-AWS-ELB-2` | Classic Load Balancer Without Instances | elb | discovery | Implemented |
| `CLDBRN-AWS-ELB-3` | Gateway Load Balancer Without Targets | elb | discovery | Implemented |
| `CLDBRN-AWS-ELB-4` | Network Load Balancer Without Targets | elb | discovery | Implemented |
| `CLDBRN-AWS-ELB-5` | Load Balancer Idle | elb | discovery | Implemented |
| `CLDBRN-AWS-EMR-1` | EMR Cluster Previous Generation Instance Types | emr | discovery | Implemented |
| `CLDBRN-AWS-EMR-1` | EMR Cluster Previous Generation Instance Types | emr | discovery, iac | Implemented |
| `CLDBRN-AWS-EMR-2` | EMR Cluster Idle | emr | discovery | Implemented |
| `CLDBRN-AWS-RDS-1` | RDS Instance Class Not Preferred | rds | iac, discovery | Implemented |
| `CLDBRN-AWS-RDS-2` | RDS DB Instance Idle | rds | discovery | Implemented |
| `CLDBRN-AWS-RDS-3` | RDS DB Instance Missing Reserved Coverage | rds | discovery | Implemented |
| `CLDBRN-AWS-RDS-4` | RDS DB Instance Without Graviton | rds | discovery | Implemented |
| `CLDBRN-AWS-RDS-4` | RDS DB Instance Without Graviton | rds | discovery, iac | Implemented |
| `CLDBRN-AWS-RDS-5` | RDS DB Instance Low CPU Utilization | rds | discovery | Implemented |
| `CLDBRN-AWS-RDS-6` | RDS DB Instance Unsupported Engine Version | rds | discovery | Implemented |
| `CLDBRN-AWS-RDS-6` | RDS DB Instance Unsupported Engine Version | rds | discovery, iac | Implemented |
| `CLDBRN-AWS-RDS-7` | RDS Snapshot Without Source DB Instance | rds | discovery | Implemented |
| `CLDBRN-AWS-REDSHIFT-1` | Redshift Cluster Low CPU Utilization | redshift | discovery | Implemented |
| `CLDBRN-AWS-REDSHIFT-2` | Redshift Cluster Missing Reserved Coverage | redshift | discovery | Implemented |
| `CLDBRN-AWS-REDSHIFT-3` | Redshift Cluster Pause Resume Not Enabled | redshift | discovery | Implemented |
| `CLDBRN-AWS-ROUTE53-1` | Route 53 Record Higher TTL | route53 | discovery | Implemented |
| `CLDBRN-AWS-ROUTE53-2` | Route 53 Health Check Unused | route53 | discovery | Implemented |
| `CLDBRN-AWS-ROUTE53-1` | Route 53 Record Higher TTL | route53 | discovery, iac | Implemented |
| `CLDBRN-AWS-ROUTE53-2` | Route 53 Health Check Unused | route53 | discovery, iac | Implemented |
| `CLDBRN-AWS-S3-1` | S3 Missing Lifecycle Configuration | s3 | iac, discovery | Implemented |
| `CLDBRN-AWS-S3-2` | S3 Bucket Storage Class Not Optimized | s3 | iac, discovery | Implemented |
| `CLDBRN-AWS-SECRETSMANAGER-1` | Secrets Manager Secret Unused | secretsmanager | discovery | Implemented |
Expand Down Expand Up @@ -164,7 +164,7 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`

`CLDBRN-AWS-ROUTE53-1` reviews only non-alias records and treats `3600` seconds as the low-TTL floor.

`CLDBRN-AWS-ROUTE53-2` flags only Route 53 health checks that are not referenced by any discovered record set.
`CLDBRN-AWS-ROUTE53-2` flags only Route 53 health checks that are not referenced by any in-scope record set.

`CLDBRN-AWS-SECRETSMANAGER-1` flags secrets with no `lastAccessedDate` and secrets whose parsed last access is at least `90` days old.

Expand Down
11 changes: 10 additions & 1 deletion packages/rules/src/aws/apigateway/caching-disabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ export const apiGatewayCachingDisabledRule = createRule({
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['discovery'],
supports: ['discovery', 'iac'],
discoveryDependencies: ['aws-apigateway-stages'],
staticDependencies: ['aws-apigateway-stages'],
evaluateLive: ({ resources }) => {
const findings = resources
.get('aws-apigateway-stages')
Expand All @@ -22,4 +23,12 @@ export const apiGatewayCachingDisabledRule = createRule({

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
},
evaluateStatic: ({ resources }) => {
const findings = resources
.get('aws-apigateway-stages')
.filter((stage) => stage.cacheClusterEnabled === false)
.map((stage) => createFindingMatch(stage.resourceId, undefined, undefined, stage.location));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
},
});
11 changes: 10 additions & 1 deletion packages/rules/src/aws/cloudfront/distribution-pricing-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ export const cloudFrontDistributionPricingClassRule = createRule({
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['discovery'],
supports: ['discovery', 'iac'],
discoveryDependencies: ['aws-cloudfront-distributions'],
staticDependencies: ['aws-cloudfront-distributions'],
evaluateLive: ({ resources }) => {
const findings = resources
.get('aws-cloudfront-distributions')
Expand All @@ -24,4 +25,12 @@ export const cloudFrontDistributionPricingClassRule = createRule({

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
},
evaluateStatic: ({ resources }) => {
const findings = resources
.get('aws-cloudfront-distributions')
.filter((distribution) => distribution.priceClass === 'PriceClass_All')
.map((distribution) => createFindingMatch(distribution.resourceId, undefined, undefined, distribution.location));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
},
});
18 changes: 16 additions & 2 deletions packages/rules/src/aws/cloudwatch/log-group-retention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ const RULE_SERVICE = 'cloudwatch';
const RULE_MESSAGE =
'CloudWatch log groups should define a retention policy unless AWS manages lifecycle automatically.';

const hasMissingRetention = (
retentionInDays: number | null | undefined,
logGroupClass: string | null | undefined,
): boolean => retentionInDays === undefined && logGroupClass !== 'DELIVERY';

/** Flag CloudWatch log groups that do not define retention and are not delivery-managed. */
export const cloudWatchLogGroupRetentionRule = createRule({
id: RULE_ID,
Expand All @@ -13,14 +18,23 @@ export const cloudWatchLogGroupRetentionRule = createRule({
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['discovery'],
supports: ['discovery', 'iac'],
discoveryDependencies: ['aws-cloudwatch-log-groups'],
staticDependencies: ['aws-cloudwatch-log-groups'],
evaluateLive: ({ resources }) => {
const findings = resources
.get('aws-cloudwatch-log-groups')
.filter((logGroup) => logGroup.retentionInDays === undefined && logGroup.logGroupClass !== 'DELIVERY')
.filter((logGroup) => hasMissingRetention(logGroup.retentionInDays, logGroup.logGroupClass))
.map((logGroup) => createFindingMatch(logGroup.logGroupName, logGroup.region, logGroup.accountId));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
},
evaluateStatic: ({ resources }) => {
const findings = resources
.get('aws-cloudwatch-log-groups')
.filter((logGroup) => hasMissingRetention(logGroup.retentionInDays, logGroup.logGroupClass))
.map((logGroup) => createFindingMatch(logGroup.resourceId, undefined, undefined, logGroup.location));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
},
});
21 changes: 20 additions & 1 deletion packages/rules/src/aws/dynamodb/table-without-autoscaling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ export const dynamoDbTableWithoutAutoscalingRule = createRule({
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['discovery'],
supports: ['discovery', 'iac'],
discoveryDependencies: ['aws-dynamodb-tables', 'aws-dynamodb-autoscaling'],
staticDependencies: ['aws-dynamodb-tables', 'aws-dynamodb-autoscaling'],
evaluateLive: ({ resources }) => {
const autoscalingByTable = new Map(
resources
Expand All @@ -36,4 +37,22 @@ export const dynamoDbTableWithoutAutoscalingRule = createRule({

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
},
evaluateStatic: ({ resources }) => {
const autoscalingByTable = new Map(
resources
.get('aws-dynamodb-autoscaling')
.filter((table) => table.tableName !== null)
.map((table) => [table.tableName, table] as const),
);
const findings = resources
.get('aws-dynamodb-tables')
.filter((table) => table.billingMode === 'PROVISIONED' && table.tableName !== null)
.filter((table) => {
const autoscaling = autoscalingByTable.get(table.tableName);
return autoscaling ? !autoscaling.hasReadTarget && !autoscaling.hasWriteTarget : true;
})
.map((table) => createFindingMatch(table.resourceId, undefined, undefined, table.location));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
},
});
26 changes: 19 additions & 7 deletions packages/rules/src/aws/ebs/high-iops-volume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ const HIGH_IOPS_VOLUME_TYPES = new Set(['io1', 'io2']);
// Treat 32k IOPS as the threshold where provisioned io1/io2 volumes merit explicit review.
const HIGH_IOPS_THRESHOLD = 32000;

const hasHighProvisionedIops = (volumeType: string | null | undefined, iops: number | null | undefined): boolean =>
volumeType !== null &&
volumeType !== undefined &&
HIGH_IOPS_VOLUME_TYPES.has(volumeType) &&
iops !== null &&
iops !== undefined &&
iops > HIGH_IOPS_THRESHOLD;

/** Flag io1 and io2 EBS volumes provisioned above the high-IOPS threshold. */
export const ebsHighIopsVolumeRule = createRule({
id: RULE_ID,
Expand All @@ -15,19 +23,23 @@ export const ebsHighIopsVolumeRule = createRule({
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['discovery'],
supports: ['discovery', 'iac'],
discoveryDependencies: ['aws-ebs-volumes'],
staticDependencies: ['aws-ebs-volumes'],
evaluateLive: ({ resources }) => {
const findings = resources
.get('aws-ebs-volumes')
.filter(
(volume) =>
HIGH_IOPS_VOLUME_TYPES.has(volume.volumeType) &&
volume.iops !== undefined &&
volume.iops > HIGH_IOPS_THRESHOLD,
)
.filter((volume) => hasHighProvisionedIops(volume.volumeType, volume.iops))
.map((volume) => createFindingMatch(volume.volumeId, volume.region, volume.accountId));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
},
evaluateStatic: ({ resources }) => {
const findings = resources
.get('aws-ebs-volumes')
.filter((volume) => hasHighProvisionedIops(volume.volumeType, volume.iops))
.map((volume) => createFindingMatch(volume.resourceId, undefined, undefined, volume.location));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
},
});
16 changes: 14 additions & 2 deletions packages/rules/src/aws/ebs/large-volume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const RULE_MESSAGE = 'EBS volumes larger than 100 GiB should be reviewed.';
// Treat volumes above 100 GiB as oversized enough to warrant an explicit cost review.
const LARGE_VOLUME_SIZE_THRESHOLD_GIB = 100;

const isLargeEbsVolume = (sizeGiB: number | null | undefined): boolean =>
sizeGiB !== null && sizeGiB !== undefined && sizeGiB > LARGE_VOLUME_SIZE_THRESHOLD_GIB;

/** Flag EBS volumes that exceed the large-volume review threshold. */
export const ebsLargeVolumeRule = createRule({
id: RULE_ID,
Expand All @@ -14,14 +17,23 @@ export const ebsLargeVolumeRule = createRule({
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['discovery'],
supports: ['discovery', 'iac'],
discoveryDependencies: ['aws-ebs-volumes'],
staticDependencies: ['aws-ebs-volumes'],
evaluateLive: ({ resources }) => {
const findings = resources
.get('aws-ebs-volumes')
.filter((volume) => volume.sizeGiB > LARGE_VOLUME_SIZE_THRESHOLD_GIB)
.filter((volume) => isLargeEbsVolume(volume.sizeGiB))
.map((volume) => createFindingMatch(volume.volumeId, volume.region, volume.accountId));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
},
evaluateStatic: ({ resources }) => {
const findings = resources
.get('aws-ebs-volumes')
.filter((volume) => isLargeEbsVolume(volume.sizeGiB))
.map((volume) => createFindingMatch(volume.resourceId, undefined, undefined, volume.location));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
},
});
Loading
Loading