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/quiet-sdk-hydrators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudburn/sdk": minor
---

Add AWS discovery datasets and hydrators for Lambda function metrics plus enriched RDS instance, reservation, CPU, and snapshot metadata needed by the new ELB, Lambda, and RDS built-in rules.
5 changes: 5 additions & 0 deletions .changeset/steady-rules-bloom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudburn/rules": minor
---

Add new AWS discovery rules for unused Network Load Balancers, Lambda error-rate and timeout review, and RDS reserved coverage, Graviton review, low CPU utilization, unsupported engine versions, and orphaned snapshots.
24 changes: 23 additions & 1 deletion docs/reference/rule-ids.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,24 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`
| `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-EMR-1` | EMR Cluster Previous Generation Instance Types | emr | discovery | 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-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-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-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-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-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`.

Expand Down Expand Up @@ -86,12 +94,26 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`

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

`CLDBRN-AWS-ELB-1` and `CLDBRN-AWS-ELB-3` flag load balancers with no attached target groups or no registered targets across attached target groups.
`CLDBRN-AWS-ELB-1`, `CLDBRN-AWS-ELB-3`, and `CLDBRN-AWS-ELB-4` flag load balancers with no attached target groups or no registered targets across attached target groups.

`CLDBRN-AWS-EMR-1` reuses the built-in EC2 family policy. EMR clusters are flagged when any discovered cluster instance type falls into the current non-preferred, previous-generation family set.

`CLDBRN-AWS-EMR-2` flags only active clusters whose `IsIdle` metric stays true for six consecutive 5-minute periods, which is a 30-minute idle window.

`CLDBRN-AWS-LAMBDA-2` uses 7-day CloudWatch totals and flags only functions whose observed `Errors / Invocations` ratio is greater than `10%`.

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

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

`CLDBRN-AWS-RDS-5` reviews only `available` DB instances and treats a complete 30-day average `CPUUtilization` of `10%` or lower as low utilization.

`CLDBRN-AWS-RDS-6` flags only RDS MySQL `5.7.x` and PostgreSQL `11.x` DB instances for extended-support review.

`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-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 Down
2 changes: 1 addition & 1 deletion packages/cloudburn/test/rules-list.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest';

describe('rules list e2e', () => {
describe('rules list e2e', { timeout: 30_000 }, () => {
afterEach(() => {
vi.resetModules();
vi.restoreAllMocks();
Expand Down
8 changes: 7 additions & 1 deletion packages/rules/src/aws/elb/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { elbAlbWithoutTargetsRule } from './alb-without-targets.js';
import { elbClassicWithoutInstancesRule } from './classic-without-instances.js';
import { elbGatewayWithoutTargetsRule } from './gateway-without-targets.js';
import { elbNetworkWithoutTargetsRule } from './network-without-targets.js';

/** Aggregate AWS ELB rule definitions. */
export const elbRules = [elbAlbWithoutTargetsRule, elbClassicWithoutInstancesRule, elbGatewayWithoutTargetsRule];
export const elbRules = [
elbAlbWithoutTargetsRule,
elbClassicWithoutInstancesRule,
elbGatewayWithoutTargetsRule,
elbNetworkWithoutTargetsRule,
];
30 changes: 30 additions & 0 deletions packages/rules/src/aws/elb/network-without-targets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';
import { hasNoRegisteredTargets } from './shared.js';

const RULE_ID = 'CLDBRN-AWS-ELB-4';
const RULE_SERVICE = 'elb';
const RULE_MESSAGE = 'Network Load Balancers with no registered targets should be deleted.';

/** Flag NLBs that have no attached target groups or only empty target groups. */
export const elbNetworkWithoutTargetsRule = createRule({
id: RULE_ID,
name: 'Network Load Balancer Without Targets',
description: 'Flag Network Load Balancers that have no attached target groups or no registered targets.',
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['discovery'],
discoveryDependencies: ['aws-ec2-load-balancers', 'aws-ec2-target-groups'],
evaluateLive: ({ resources }) => {
const targetGroups = resources.get('aws-ec2-target-groups');
const findings = resources
.get('aws-ec2-load-balancers')
.filter((loadBalancer) => loadBalancer.loadBalancerType === 'network')
.filter((loadBalancer) => hasNoRegisteredTargets(loadBalancer, targetGroups))
.map((loadBalancer) =>
createFindingMatch(loadBalancer.loadBalancerArn, loadBalancer.region, loadBalancer.accountId),
);

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

const RULE_ID = 'CLDBRN-AWS-LAMBDA-3';
const RULE_SERVICE = 'lambda';
const RULE_MESSAGE = 'Lambda functions should not keep timeouts far above their observed average duration.';
// Review only generously configured functions whose timeout is at least 30s and 5x the observed average duration.
const MIN_TIMEOUT_REVIEW_SECONDS = 30;
const EXCESSIVE_TIMEOUT_RATIO = 5;
const getFunctionKey = (accountId: string, region: string, functionName: string): string =>
`${accountId}:${region}:${functionName}`;

/** Flag Lambda functions whose configured timeout far exceeds observed average execution time. */
export const lambdaExcessiveTimeoutRule = createRule({
id: RULE_ID,
name: 'Lambda Function Excessive Timeout',
description:
'Flag Lambda functions whose configured timeout is at least 30 seconds and 5x their 7-day average duration.',
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['discovery'],
discoveryDependencies: ['aws-lambda-functions', 'aws-lambda-function-metrics'],
evaluateLive: ({ resources }) => {
const metricsByFunctionKey = new Map(
resources
.get('aws-lambda-function-metrics')
.map((metric) => [getFunctionKey(metric.accountId, metric.region, metric.functionName), metric] as const),
);

const findings = resources
.get('aws-lambda-functions')
.filter((fn) => {
const metric = metricsByFunctionKey.get(getFunctionKey(fn.accountId, fn.region, fn.functionName));

return (
fn.timeoutSeconds >= MIN_TIMEOUT_REVIEW_SECONDS &&
metric?.averageDurationMsLast7Days !== null &&
metric?.averageDurationMsLast7Days !== undefined &&
metric.averageDurationMsLast7Days > 0 &&
fn.timeoutSeconds * 1000 >= metric.averageDurationMsLast7Days * EXCESSIVE_TIMEOUT_RATIO
);
})
.map((fn) => createFindingMatch(fn.functionName, fn.region, fn.accountId));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
},
});
46 changes: 46 additions & 0 deletions packages/rules/src/aws/lambda/high-error-rate.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-LAMBDA-2';
const RULE_SERVICE = 'lambda';
const RULE_MESSAGE = 'Lambda functions should not sustain an error rate above 10% over the last 7 days.';
// Error-rate review requires complete 7-day totals and only flags functions above 10%.
const HIGH_ERROR_RATE_THRESHOLD = 0.1;
const getFunctionKey = (accountId: string, region: string, functionName: string): string =>
`${accountId}:${region}:${functionName}`;

/** Flag Lambda functions whose recent error rate exceeds the review threshold. */
export const lambdaHighErrorRateRule = createRule({
id: RULE_ID,
name: 'Lambda Function High Error Rate',
description: 'Flag Lambda functions whose 7-day error rate is greater than 10%.',
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['discovery'],
discoveryDependencies: ['aws-lambda-functions', 'aws-lambda-function-metrics'],
evaluateLive: ({ resources }) => {
const metricsByFunctionKey = new Map(
resources
.get('aws-lambda-function-metrics')
.map((metric) => [getFunctionKey(metric.accountId, metric.region, metric.functionName), metric] as const),
);

const findings = resources
.get('aws-lambda-functions')
.filter((fn) => {
const metric = metricsByFunctionKey.get(getFunctionKey(fn.accountId, fn.region, fn.functionName));

return (
metric?.totalInvocationsLast7Days !== null &&
metric?.totalInvocationsLast7Days !== undefined &&
metric.totalInvocationsLast7Days > 0 &&
metric.totalErrorsLast7Days !== null &&
metric.totalErrorsLast7Days !== undefined &&
metric.totalErrorsLast7Days / metric.totalInvocationsLast7Days > HIGH_ERROR_RATE_THRESHOLD
);
})
.map((fn) => createFindingMatch(fn.functionName, fn.region, fn.accountId));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
},
});
4 changes: 3 additions & 1 deletion packages/rules/src/aws/lambda/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { lambdaCostOptimalArchitectureRule } from './cost-optimal-architecture.js';
import { lambdaExcessiveTimeoutRule } from './excessive-timeout.js';
import { lambdaHighErrorRateRule } from './high-error-rate.js';

// Intent: aggregate AWS Lambda rule definitions.
// TODO(cloudburn): add memory-rightsizing and idle-function checks.
export const lambdaRules = [lambdaCostOptimalArchitectureRule];
export const lambdaRules = [lambdaCostOptimalArchitectureRule, lambdaHighErrorRateRule, lambdaExcessiveTimeoutRule];
31 changes: 31 additions & 0 deletions packages/rules/src/aws/rds/graviton-review.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';
import { isAwsRdsGravitonFamily, shouldReviewAwsRdsInstanceClassForGraviton } from './preferred-instance-families.js';

const RULE_ID = 'CLDBRN-AWS-RDS-4';
const RULE_SERVICE = 'rds';
const RULE_MESSAGE = 'RDS DB instances without a Graviton equivalent in use should be reviewed.';

/** Flag RDS DB instances still using reviewable non-Graviton families. */
export const rdsGravitonReviewRule = createRule({
id: RULE_ID,
name: 'RDS DB Instance Without Graviton',
description:
'Flag RDS DB instances that still use non-Graviton instance families when a clear Graviton-based equivalent exists.',
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['discovery'],
discoveryDependencies: ['aws-rds-instances'],
evaluateLive: ({ resources }) => {
const findings = resources
.get('aws-rds-instances')
.filter(
(instance) =>
!isAwsRdsGravitonFamily(instance.instanceClass) &&
shouldReviewAwsRdsInstanceClassForGraviton(instance.instanceClass),
)
.map((instance) => createFindingMatch(instance.dbInstanceIdentifier, instance.region, instance.accountId));

return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
},
});
15 changes: 14 additions & 1 deletion packages/rules/src/aws/rds/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { rdsGravitonReviewRule } from './graviton-review.js';
import { rdsIdleInstanceRule } from './idle-instance.js';
import { rdsLowCpuUtilizationRule } from './low-cpu-utilization.js';
import { rdsPreferredInstanceClassRule } from './preferred-instance-classes.js';
import { rdsReservedCoverageRule } from './reserved-coverage.js';
import { rdsUnsupportedEngineVersionRule } from './unsupported-engine-version.js';
import { rdsUnusedSnapshotsRule } from './unused-snapshots.js';

// Intent: aggregate AWS RDS rule definitions.
// TODO(cloudburn): add idle-instance and single-AZ production checks.
export const rdsRules = [rdsPreferredInstanceClassRule, rdsIdleInstanceRule];
export const rdsRules = [
rdsPreferredInstanceClassRule,
rdsIdleInstanceRule,
rdsReservedCoverageRule,
rdsGravitonReviewRule,
rdsLowCpuUtilizationRule,
rdsUnsupportedEngineVersionRule,
rdsUnusedSnapshotsRule,
];
54 changes: 54 additions & 0 deletions packages/rules/src/aws/rds/low-cpu-utilization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';

const RULE_ID = 'CLDBRN-AWS-RDS-5';
const RULE_SERVICE = 'rds';
const RULE_MESSAGE = 'RDS DB instances with low CPU utilization should be reviewed.';
// Review provisioned databases whose 30-day average CPU stays at or below 10%.
const LOW_CPU_THRESHOLD = 10;
const getInstanceKey = (accountId: string, region: string, dbInstanceIdentifier: string): string =>
`${accountId}:${region}:${dbInstanceIdentifier}`;

/** Flag available RDS DB instances with sustained low CPU utilization. */
export const rdsLowCpuUtilizationRule = createRule({
id: RULE_ID,
name: 'RDS DB Instance Low CPU Utilization',
description: 'Flag available RDS DB instances whose 30-day average CPU stays at or below 10%.',
message: RULE_MESSAGE,
provider: 'aws',
service: RULE_SERVICE,
supports: ['discovery'],
discoveryDependencies: ['aws-rds-instances', 'aws-rds-instance-cpu-metrics'],
evaluateLive: ({ resources }) => {
const instancesById = new Map(
resources
.get('aws-rds-instances')
.map(
(instance) =>
[getInstanceKey(instance.accountId, instance.region, instance.dbInstanceIdentifier), instance] as const,
),
);

const findings = resources
.get('aws-rds-instance-cpu-metrics')
.filter((metric) => {
const instance = instancesById.get(
getInstanceKey(metric.accountId, metric.region, metric.dbInstanceIdentifier),
);

return (
instance?.dbInstanceStatus === 'available' &&
metric.averageCpuUtilizationLast30Days !== null &&
metric.averageCpuUtilizationLast30Days <= LOW_CPU_THRESHOLD
);
})
.flatMap((metric) => {
const instance = instancesById.get(
getInstanceKey(metric.accountId, metric.region, metric.dbInstanceIdentifier),
);

return instance ? [createFindingMatch(instance.dbInstanceIdentifier, instance.region, instance.accountId)] : [];
});

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