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/aws-iac-cost-rules-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudburn/rules": minor
---

Add new AWS IaC cost review rules for versioned S3 cleanup, ECR lifecycle quality, gp3 tuning, EC2 detailed monitoring, DynamoDB autoscaling ranges, Lambda provisioned concurrency, and RDS Performance Insights retention, and extend ECS and Redshift rules to support IaC.
5 changes: 5 additions & 0 deletions .changeset/aws-iac-cost-rules-sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudburn/sdk": minor
---

Add AWS static IaC dataset support for new cost review rules across S3, ECR, EBS, EC2, DynamoDB, ECS, Lambda, RDS, and Redshift.
31 changes: 29 additions & 2 deletions docs/reference/rule-ids.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`
| `CLDBRN-AWS-DYNAMODB-1` | DynamoDB Table Stale Data | 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-DYNAMODB-4` | DynamoDB Autoscaling Range Fixed | dynamodb | iac | 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, iac | Implemented |
Expand All @@ -39,17 +40,22 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`
| `CLDBRN-AWS-EC2-7` | EC2 Reserved Instance Expiring | 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-EC2-10` | EC2 Instance Detailed Monitoring Enabled | ec2 | iac | 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-ECS-3` | ECS Service Missing Autoscaling Policy | ecs | discovery, iac | 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, 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-EBS-8` | EBS gp3 Volume Extra Throughput Provisioned | ebs | iac | Implemented |
| `CLDBRN-AWS-EBS-9` | EBS gp3 Volume Extra IOPS Provisioned | ebs | iac | Implemented |
| `CLDBRN-AWS-ECR-1` | ECR Repository Missing Lifecycle Policy | ecr | iac, discovery | Implemented |
| `CLDBRN-AWS-ECR-2` | ECR Lifecycle Policy Missing Untagged Image Expiry | ecr | iac | Implemented |
| `CLDBRN-AWS-ECR-3` | ECR Lifecycle Policy Missing Tagged Image Retention Cap | ecr | iac | 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 |
Expand All @@ -67,19 +73,22 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`
| `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, iac | Implemented |
| `CLDBRN-AWS-RDS-7` | RDS Snapshot Without Source DB Instance | rds | discovery | Implemented |
| `CLDBRN-AWS-RDS-8` | RDS Performance Insights Extended Retention | rds | iac | 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-REDSHIFT-3` | Redshift Cluster Pause Resume Not Enabled | redshift | discovery, iac | 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-S3-3` | S3 Incomplete Multipart Upload Abort Configuration | s3 | iac, discovery | Implemented |
| `CLDBRN-AWS-S3-4` | S3 Versioned Bucket Missing Noncurrent Version Cleanup | s3 | iac | Implemented |
| `CLDBRN-AWS-SECRETSMANAGER-1` | Secrets Manager Secret Unused | secretsmanager | discovery | Implemented |
| `CLDBRN-AWS-LAMBDA-1` | Lambda Cost Optimal Architecture | lambda | iac, discovery | Implemented |
| `CLDBRN-AWS-LAMBDA-2` | Lambda Function High Error Rate | lambda | discovery | Implemented |
| `CLDBRN-AWS-LAMBDA-3` | Lambda Function Excessive Timeout | lambda | discovery | Implemented |
| `CLDBRN-AWS-LAMBDA-4` | Lambda Function Memory Overprovisioned | lambda | discovery | Implemented |
| `CLDBRN-AWS-LAMBDA-5` | Lambda Provisioned Concurrency Configured | lambda | iac | Implemented |

`CLDBRN-AWS-APIGATEWAY-1` flags REST API stages when `cacheClusterEnabled` is not explicitly `true`.

Expand Down Expand Up @@ -113,6 +122,8 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`

`CLDBRN-AWS-DYNAMODB-3` reviews only provisioned-capacity tables and flags them when 30 days of consumed read and write capacity both sum to zero.

`CLDBRN-AWS-DYNAMODB-4` reviews only provisioned-capacity tables and flags them when statically resolved read or write autoscaling ranges have identical min and max capacity values.

`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.

`CLDBRN-AWS-EC2-7` reviews only active reserved instances with an `endTime` inside the next 60 days.
Expand All @@ -121,12 +132,22 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`

`CLDBRN-AWS-EC2-9` flags only instances with a parsed launch timestamp at least 180 days old.

`CLDBRN-AWS-EC2-10` flags IaC-defined instances only when detailed monitoring is explicitly enabled.

`CLDBRN-AWS-ECS-1` flags only EC2-backed container instances whose instance families have a curated Graviton-equivalent path. Fargate and unclassified backing instances are skipped.

`CLDBRN-AWS-ECS-2` flags only ECS clusters with a complete 14-day `AWS/ECS` CPU history and an average below `10%`.

`CLDBRN-AWS-ECS-3` flags only active `REPLICA` ECS services and requires both a scalable target and at least one scaling policy.

`CLDBRN-AWS-EBS-8` flags only `gp3` volumes whose provisioned throughput is above the included `125 MiB/s` baseline.

`CLDBRN-AWS-EBS-9` flags only `gp3` volumes whose provisioned or defaulted IOPS exceed the included `3000` baseline.

`CLDBRN-AWS-ECR-2` reviews only repositories with a lifecycle policy and flags them when the statically parsed policy does not expire untagged images.

`CLDBRN-AWS-ECR-3` reviews only repositories with a lifecycle policy and flags them when the statically parsed policy does not cap tagged image retention.

`CLDBRN-AWS-EKS-1` flags only managed node groups with classifiable non-Arm instance families. Arm AMIs and unclassified node groups are skipped.

`CLDBRN-AWS-ELASTICACHE-1` reviews only `available` clusters with a parsed create time at least 180 days old and requires active reserved-node capacity on the same node type, preferring exact engine matches when ElastiCache reports them.
Expand All @@ -147,6 +168,8 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`

`CLDBRN-AWS-LAMBDA-4` reviews only functions configured above `256 MB`, requires invocation history, and flags them when the observed 7-day average duration uses less than `30%` of the configured timeout.

`CLDBRN-AWS-LAMBDA-5` flags explicit provisioned concurrency configuration when provisioned concurrent executions are greater than zero.

`CLDBRN-AWS-RDS-3` reviews only `available` DB instances with a parsed create time at least 180 days old and requires active reserved-instance coverage on the same instance class, deployment mode, and normalized engine when AWS reports it.

`CLDBRN-AWS-RDS-4` flags only curated non-Graviton RDS families with a clear Graviton migration path. Existing Graviton classes and unclassified families are skipped.
Expand All @@ -157,6 +180,8 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`

`CLDBRN-AWS-RDS-7` flags only snapshots whose source DB instance no longer exists and whose parsed create time is at least `30` days old.

`CLDBRN-AWS-RDS-8` flags only DB instances with Performance Insights enabled and a retention period above the included 7-day baseline.

`CLDBRN-AWS-REDSHIFT-1` reviews only `available` clusters and treats a 14-day average `CPUUtilization` of 10% or lower as low utilization.

`CLDBRN-AWS-REDSHIFT-2` reviews only `available` clusters with a parsed create time at least 180 days old and requires active reserved-node coverage for the same node type.
Expand All @@ -169,6 +194,8 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`

`CLDBRN-AWS-S3-3` flags buckets when no enabled lifecycle rule aborts incomplete multipart uploads within 7 days.

`CLDBRN-AWS-S3-4` flags only versioned buckets and requires either noncurrent-version expiration or transition cleanup to avoid unbounded version growth.

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

**Status key:**
Expand Down
46 changes: 46 additions & 0 deletions packages/rules/src/aws/dynamodb/autoscaling-range-fixed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';

const RULE_ID = 'CLDBRN-AWS-DYNAMODB-4';
const RULE_SERVICE = 'dynamodb';
const RULE_MESSAGE = 'Provisioned DynamoDB autoscaling should allow capacity to change.';

const hasFixedRange = (minCapacity: number | null | undefined, maxCapacity: number | null | undefined): boolean =>
typeof minCapacity === 'number' && typeof maxCapacity === 'number' && minCapacity === maxCapacity;

/** Flag provisioned-capacity DynamoDB tables whose table autoscaling min and max capacity are identical. */
export const dynamoDbAutoscalingRangeFixedRule = createRule({
id: RULE_ID,
name: 'DynamoDB Autoscaling Range Fixed',
description: 'Flag provisioned-capacity DynamoDB tables whose table autoscaling min and max capacity are identical.',
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['iac'],
staticDependencies: ['aws-dynamodb-tables', 'aws-dynamodb-autoscaling'],
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);

if (!autoscaling) {
return false;
}

return (
hasFixedRange(autoscaling.readMinCapacity, autoscaling.readMaxCapacity) ||
hasFixedRange(autoscaling.writeMinCapacity, autoscaling.writeMaxCapacity)
);
})
.map((table) => createFindingMatch(table.resourceId, undefined, undefined, table.location));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
},
});
8 changes: 7 additions & 1 deletion packages/rules/src/aws/dynamodb/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { dynamoDbAutoscalingRangeFixedRule } from './autoscaling-range-fixed.js';
import { dynamoDbStaleTableDataRule } from './stale-table-data.js';
import { dynamoDbTableWithoutAutoscalingRule } from './table-without-autoscaling.js';
import { dynamoDbUnusedTableRule } from './unused-table.js';

// Intent: aggregate AWS DynamoDB rule definitions.
export const dynamodbRules = [dynamoDbStaleTableDataRule, dynamoDbTableWithoutAutoscalingRule, dynamoDbUnusedTableRule];
export const dynamodbRules = [
dynamoDbStaleTableDataRule,
dynamoDbTableWithoutAutoscalingRule,
dynamoDbUnusedTableRule,
dynamoDbAutoscalingRangeFixedRule,
];
25 changes: 25 additions & 0 deletions packages/rules/src/aws/ebs/gp3-extra-iops.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';

const RULE_ID = 'CLDBRN-AWS-EBS-9';
const RULE_SERVICE = 'ebs';
const RULE_MESSAGE = 'EBS gp3 volumes should avoid paid IOPS above the included baseline unless required.';

/** Flag gp3 volumes that provision IOPS above the included 3000 baseline. */
export const ebsGp3ExtraIopsRule = createRule({
id: RULE_ID,
name: 'EBS gp3 Volume Extra IOPS Provisioned',
description: 'Flag gp3 volumes that provision IOPS above the included 3000 baseline.',
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['iac'],
staticDependencies: ['aws-ebs-volumes'],
evaluateStatic: ({ resources }) => {
const findings = resources
.get('aws-ebs-volumes')
.filter((volume) => volume.volumeType === 'gp3' && volume.iops !== null && volume.iops > 3000)
.map((volume) => createFindingMatch(volume.resourceId, undefined, undefined, volume.location));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
},
});
28 changes: 28 additions & 0 deletions packages/rules/src/aws/ebs/gp3-extra-throughput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';

const RULE_ID = 'CLDBRN-AWS-EBS-8';
const RULE_SERVICE = 'ebs';
const RULE_MESSAGE = 'EBS gp3 volumes should avoid paid throughput above the included baseline unless required.';

/** Flag gp3 volumes that provision throughput above the included 125 MiB/s baseline. */
export const ebsGp3ExtraThroughputRule = createRule({
id: RULE_ID,
name: 'EBS gp3 Volume Extra Throughput Provisioned',
description: 'Flag gp3 volumes that provision throughput above the included 125 MiB/s baseline.',
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['iac'],
staticDependencies: ['aws-ebs-volumes'],
evaluateStatic: ({ resources }) => {
const findings = resources
.get('aws-ebs-volumes')
.filter(
(volume) =>
volume.volumeType === 'gp3' && typeof volume.throughputMiBps === 'number' && volume.throughputMiBps > 125,
)
.map((volume) => createFindingMatch(volume.resourceId, undefined, undefined, volume.location));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
},
});
4 changes: 4 additions & 0 deletions packages/rules/src/aws/ebs/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ebsAttachedToStoppedInstancesRule } from './attached-to-stopped-instances.js';
import { ebsGp3ExtraIopsRule } from './gp3-extra-iops.js';
import { ebsGp3ExtraThroughputRule } from './gp3-extra-throughput.js';
import { ebsHighIopsVolumeRule } from './high-iops-volume.js';
import { ebsLargeVolumeRule } from './large-volume.js';
import { ebsLowIopsVolumeRule } from './low-iops-volume.js';
Expand All @@ -15,4 +17,6 @@ export const ebsRules = [
ebsHighIopsVolumeRule,
ebsLowIopsVolumeRule,
ebsSnapshotMaxAgeRule,
ebsGp3ExtraThroughputRule,
ebsGp3ExtraIopsRule,
];
25 changes: 25 additions & 0 deletions packages/rules/src/aws/ec2/detailed-monitoring-enabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';

const RULE_ID = 'CLDBRN-AWS-EC2-10';
const RULE_SERVICE = 'ec2';
const RULE_MESSAGE = 'EC2 instances should review detailed monitoring because it adds CloudWatch cost.';

/** Flag EC2 instances that explicitly enable detailed monitoring. */
export const ec2DetailedMonitoringEnabledRule = createRule({
id: RULE_ID,
name: 'EC2 Instance Detailed Monitoring Enabled',
description: 'Flag EC2 instances that explicitly enable detailed monitoring.',
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['iac'],
staticDependencies: ['aws-ec2-instances'],
evaluateStatic: ({ resources }) => {
const findings = resources
.get('aws-ec2-instances')
.filter((instance) => instance.detailedMonitoringEnabled)
.map((instance) => createFindingMatch(instance.resourceId, undefined, undefined, instance.location));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
},
});
2 changes: 2 additions & 0 deletions packages/rules/src/aws/ec2/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ec2DetailedMonitoringEnabledRule } from './detailed-monitoring-enabled.js';
import { ec2GravitonReviewRule } from './graviton-review.js';
import { ec2InactiveVpcInterfaceEndpointRule } from './inactive-vpc-interface-endpoint.js';
import { ec2LargeInstanceRule } from './large-instance.js';
Expand All @@ -19,4 +20,5 @@ export const ec2Rules = [
ec2ReservedInstanceExpiringRule,
ec2LargeInstanceRule,
ec2LongRunningInstanceRule,
ec2DetailedMonitoringEnabledRule,
];
8 changes: 7 additions & 1 deletion packages/rules/src/aws/ecr/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { ecrMissingLifecyclePolicyRule } from './missing-lifecycle-policy.js';
import { ecrMissingTaggedImageRetentionCapRule } from './missing-tagged-image-retention-cap.js';
import { ecrMissingUntaggedImageExpiryRule } from './missing-untagged-image-expiry.js';

/** Aggregate AWS ECR rule definitions. */
export const ecrRules = [ecrMissingLifecyclePolicyRule];
export const ecrRules = [
ecrMissingLifecyclePolicyRule,
ecrMissingUntaggedImageExpiryRule,
ecrMissingTaggedImageRetentionCapRule,
];
25 changes: 25 additions & 0 deletions packages/rules/src/aws/ecr/missing-tagged-image-retention-cap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';

const RULE_ID = 'CLDBRN-AWS-ECR-3';
const RULE_SERVICE = 'ecr';
const RULE_MESSAGE = 'ECR repositories should cap tagged image retention.';

/** Flag ECR repositories whose statically parsed lifecycle policy does not cap tagged image retention. */
export const ecrMissingTaggedImageRetentionCapRule = createRule({
id: RULE_ID,
name: 'ECR Lifecycle Policy Missing Tagged Image Retention Cap',
description: 'Flag ECR repositories whose lifecycle policy does not cap tagged image retention.',
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['iac'],
staticDependencies: ['aws-ecr-repositories'],
evaluateStatic: ({ resources }) => {
const findings = resources
.get('aws-ecr-repositories')
.filter((repository) => repository.hasLifecyclePolicy && repository.hasTaggedImageRetentionCap === false)
.map((repository) => createFindingMatch(repository.resourceId, undefined, undefined, repository.location));

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