Skip to content

Commit 9413ce1

Browse files
authored
feat: add AWS discovery rules for ELB, Lambda, and RDS (#45)
* feat: add AWS discovery rules for ELB, Lambda, and RDS * fix: address PR review findings - scope Lambda and RDS discovery joins by account and region - scope RDS reserved coverage by account-region seeds - add regression tests for duplicate identifiers across accounts and regions * fix(rules): address PR review findings - classify rds m8i and r8i families as preferred - extend preferred-class tests for current-generation intel families * fix(rules): address PR review findings - normalize RDS reserved engine descriptions with license suffixes - add oracle reserved coverage regression coverage * fix(sdk): address PR review findings - hydrate RDS reserved instances once per region - align reserved instance tests with single-account discovery
1 parent 9c61264 commit 9413ce1

43 files changed

Lines changed: 2732 additions & 24 deletions

Some content is hidden

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

.changeset/quiet-sdk-hydrators.md

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

.changeset/steady-rules-bloom.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 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.

docs/reference/rule-ids.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,24 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`
4545
| `CLDBRN-AWS-ELB-1` | Application Load Balancer Without Targets | elb | discovery | Implemented |
4646
| `CLDBRN-AWS-ELB-2` | Classic Load Balancer Without Instances | elb | discovery | Implemented |
4747
| `CLDBRN-AWS-ELB-3` | Gateway Load Balancer Without Targets | elb | discovery | Implemented |
48+
| `CLDBRN-AWS-ELB-4` | Network Load Balancer Without Targets | elb | discovery | Implemented |
4849
| `CLDBRN-AWS-EMR-1` | EMR Cluster Previous Generation Instance Types | emr | discovery | Implemented |
4950
| `CLDBRN-AWS-EMR-2` | EMR Cluster Idle | emr | discovery | Implemented |
5051
| `CLDBRN-AWS-RDS-1` | RDS Instance Class Not Preferred | rds | iac, discovery | Implemented |
5152
| `CLDBRN-AWS-RDS-2` | RDS DB Instance Idle | rds | discovery | Implemented |
53+
| `CLDBRN-AWS-RDS-3` | RDS DB Instance Missing Reserved Coverage | rds | discovery | Implemented |
54+
| `CLDBRN-AWS-RDS-4` | RDS DB Instance Without Graviton | rds | discovery | Implemented |
55+
| `CLDBRN-AWS-RDS-5` | RDS DB Instance Low CPU Utilization | rds | discovery | Implemented |
56+
| `CLDBRN-AWS-RDS-6` | RDS DB Instance Unsupported Engine Version | rds | discovery | Implemented |
57+
| `CLDBRN-AWS-RDS-7` | RDS Snapshot Without Source DB Instance | rds | discovery | Implemented |
5258
| `CLDBRN-AWS-REDSHIFT-1` | Redshift Cluster Low CPU Utilization | redshift | discovery | Implemented |
5359
| `CLDBRN-AWS-REDSHIFT-2` | Redshift Cluster Missing Reserved Coverage | redshift | discovery | Implemented |
5460
| `CLDBRN-AWS-REDSHIFT-3` | Redshift Cluster Pause Resume Not Enabled | redshift | discovery | Implemented |
5561
| `CLDBRN-AWS-S3-1` | S3 Missing Lifecycle Configuration | s3 | iac, discovery | Implemented |
5662
| `CLDBRN-AWS-S3-2` | S3 Bucket Storage Class Not Optimized | s3 | iac, discovery | Implemented |
5763
| `CLDBRN-AWS-LAMBDA-1` | Lambda Cost Optimal Architecture | lambda | iac, discovery | Implemented |
64+
| `CLDBRN-AWS-LAMBDA-2` | Lambda Function High Error Rate | lambda | discovery | Implemented |
65+
| `CLDBRN-AWS-LAMBDA-3` | Lambda Function Excessive Timeout | lambda | discovery | Implemented |
5866

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

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

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

89-
`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.
97+
`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.
9098

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

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

103+
`CLDBRN-AWS-LAMBDA-2` uses 7-day CloudWatch totals and flags only functions whose observed `Errors / Invocations` ratio is greater than `10%`.
104+
105+
`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.
106+
107+
`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.
108+
109+
`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.
110+
111+
`CLDBRN-AWS-RDS-5` reviews only `available` DB instances and treats a complete 30-day average `CPUUtilization` of `10%` or lower as low utilization.
112+
113+
`CLDBRN-AWS-RDS-6` flags only RDS MySQL `5.7.x` and PostgreSQL `11.x` DB instances for extended-support review.
114+
115+
`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.
116+
95117
`CLDBRN-AWS-REDSHIFT-1` reviews only `available` clusters and treats a 14-day average `CPUUtilization` of 10% or lower as low utilization.
96118

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

packages/cloudburn/test/rules-list.e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { afterEach, describe, expect, it, vi } from 'vitest';
22

3-
describe('rules list e2e', () => {
3+
describe('rules list e2e', { timeout: 30_000 }, () => {
44
afterEach(() => {
55
vi.resetModules();
66
vi.restoreAllMocks();
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { elbAlbWithoutTargetsRule } from './alb-without-targets.js';
22
import { elbClassicWithoutInstancesRule } from './classic-without-instances.js';
33
import { elbGatewayWithoutTargetsRule } from './gateway-without-targets.js';
4+
import { elbNetworkWithoutTargetsRule } from './network-without-targets.js';
45

56
/** Aggregate AWS ELB rule definitions. */
6-
export const elbRules = [elbAlbWithoutTargetsRule, elbClassicWithoutInstancesRule, elbGatewayWithoutTargetsRule];
7+
export const elbRules = [
8+
elbAlbWithoutTargetsRule,
9+
elbClassicWithoutInstancesRule,
10+
elbGatewayWithoutTargetsRule,
11+
elbNetworkWithoutTargetsRule,
12+
];
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';
2+
import { hasNoRegisteredTargets } from './shared.js';
3+
4+
const RULE_ID = 'CLDBRN-AWS-ELB-4';
5+
const RULE_SERVICE = 'elb';
6+
const RULE_MESSAGE = 'Network Load Balancers with no registered targets should be deleted.';
7+
8+
/** Flag NLBs that have no attached target groups or only empty target groups. */
9+
export const elbNetworkWithoutTargetsRule = createRule({
10+
id: RULE_ID,
11+
name: 'Network Load Balancer Without Targets',
12+
description: 'Flag Network Load Balancers that have no attached target groups or no registered targets.',
13+
message: RULE_MESSAGE,
14+
provider: 'aws',
15+
service: RULE_SERVICE,
16+
supports: ['discovery'],
17+
discoveryDependencies: ['aws-ec2-load-balancers', 'aws-ec2-target-groups'],
18+
evaluateLive: ({ resources }) => {
19+
const targetGroups = resources.get('aws-ec2-target-groups');
20+
const findings = resources
21+
.get('aws-ec2-load-balancers')
22+
.filter((loadBalancer) => loadBalancer.loadBalancerType === 'network')
23+
.filter((loadBalancer) => hasNoRegisteredTargets(loadBalancer, targetGroups))
24+
.map((loadBalancer) =>
25+
createFindingMatch(loadBalancer.loadBalancerArn, loadBalancer.region, loadBalancer.accountId),
26+
);
27+
28+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
29+
},
30+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';
2+
3+
const RULE_ID = 'CLDBRN-AWS-LAMBDA-3';
4+
const RULE_SERVICE = 'lambda';
5+
const RULE_MESSAGE = 'Lambda functions should not keep timeouts far above their observed average duration.';
6+
// Review only generously configured functions whose timeout is at least 30s and 5x the observed average duration.
7+
const MIN_TIMEOUT_REVIEW_SECONDS = 30;
8+
const EXCESSIVE_TIMEOUT_RATIO = 5;
9+
const getFunctionKey = (accountId: string, region: string, functionName: string): string =>
10+
`${accountId}:${region}:${functionName}`;
11+
12+
/** Flag Lambda functions whose configured timeout far exceeds observed average execution time. */
13+
export const lambdaExcessiveTimeoutRule = createRule({
14+
id: RULE_ID,
15+
name: 'Lambda Function Excessive Timeout',
16+
description:
17+
'Flag Lambda functions whose configured timeout is at least 30 seconds and 5x their 7-day average duration.',
18+
message: RULE_MESSAGE,
19+
provider: 'aws',
20+
service: RULE_SERVICE,
21+
supports: ['discovery'],
22+
discoveryDependencies: ['aws-lambda-functions', 'aws-lambda-function-metrics'],
23+
evaluateLive: ({ resources }) => {
24+
const metricsByFunctionKey = new Map(
25+
resources
26+
.get('aws-lambda-function-metrics')
27+
.map((metric) => [getFunctionKey(metric.accountId, metric.region, metric.functionName), metric] as const),
28+
);
29+
30+
const findings = resources
31+
.get('aws-lambda-functions')
32+
.filter((fn) => {
33+
const metric = metricsByFunctionKey.get(getFunctionKey(fn.accountId, fn.region, fn.functionName));
34+
35+
return (
36+
fn.timeoutSeconds >= MIN_TIMEOUT_REVIEW_SECONDS &&
37+
metric?.averageDurationMsLast7Days !== null &&
38+
metric?.averageDurationMsLast7Days !== undefined &&
39+
metric.averageDurationMsLast7Days > 0 &&
40+
fn.timeoutSeconds * 1000 >= metric.averageDurationMsLast7Days * EXCESSIVE_TIMEOUT_RATIO
41+
);
42+
})
43+
.map((fn) => createFindingMatch(fn.functionName, fn.region, fn.accountId));
44+
45+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
46+
},
47+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';
2+
3+
const RULE_ID = 'CLDBRN-AWS-LAMBDA-2';
4+
const RULE_SERVICE = 'lambda';
5+
const RULE_MESSAGE = 'Lambda functions should not sustain an error rate above 10% over the last 7 days.';
6+
// Error-rate review requires complete 7-day totals and only flags functions above 10%.
7+
const HIGH_ERROR_RATE_THRESHOLD = 0.1;
8+
const getFunctionKey = (accountId: string, region: string, functionName: string): string =>
9+
`${accountId}:${region}:${functionName}`;
10+
11+
/** Flag Lambda functions whose recent error rate exceeds the review threshold. */
12+
export const lambdaHighErrorRateRule = createRule({
13+
id: RULE_ID,
14+
name: 'Lambda Function High Error Rate',
15+
description: 'Flag Lambda functions whose 7-day error rate is greater than 10%.',
16+
message: RULE_MESSAGE,
17+
provider: 'aws',
18+
service: RULE_SERVICE,
19+
supports: ['discovery'],
20+
discoveryDependencies: ['aws-lambda-functions', 'aws-lambda-function-metrics'],
21+
evaluateLive: ({ resources }) => {
22+
const metricsByFunctionKey = new Map(
23+
resources
24+
.get('aws-lambda-function-metrics')
25+
.map((metric) => [getFunctionKey(metric.accountId, metric.region, metric.functionName), metric] as const),
26+
);
27+
28+
const findings = resources
29+
.get('aws-lambda-functions')
30+
.filter((fn) => {
31+
const metric = metricsByFunctionKey.get(getFunctionKey(fn.accountId, fn.region, fn.functionName));
32+
33+
return (
34+
metric?.totalInvocationsLast7Days !== null &&
35+
metric?.totalInvocationsLast7Days !== undefined &&
36+
metric.totalInvocationsLast7Days > 0 &&
37+
metric.totalErrorsLast7Days !== null &&
38+
metric.totalErrorsLast7Days !== undefined &&
39+
metric.totalErrorsLast7Days / metric.totalInvocationsLast7Days > HIGH_ERROR_RATE_THRESHOLD
40+
);
41+
})
42+
.map((fn) => createFindingMatch(fn.functionName, fn.region, fn.accountId));
43+
44+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
45+
},
46+
});
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { lambdaCostOptimalArchitectureRule } from './cost-optimal-architecture.js';
2+
import { lambdaExcessiveTimeoutRule } from './excessive-timeout.js';
3+
import { lambdaHighErrorRateRule } from './high-error-rate.js';
24

35
// Intent: aggregate AWS Lambda rule definitions.
46
// TODO(cloudburn): add memory-rightsizing and idle-function checks.
5-
export const lambdaRules = [lambdaCostOptimalArchitectureRule];
7+
export const lambdaRules = [lambdaCostOptimalArchitectureRule, lambdaHighErrorRateRule, lambdaExcessiveTimeoutRule];
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';
2+
import { isAwsRdsGravitonFamily, shouldReviewAwsRdsInstanceClassForGraviton } from './preferred-instance-families.js';
3+
4+
const RULE_ID = 'CLDBRN-AWS-RDS-4';
5+
const RULE_SERVICE = 'rds';
6+
const RULE_MESSAGE = 'RDS DB instances without a Graviton equivalent in use should be reviewed.';
7+
8+
/** Flag RDS DB instances still using reviewable non-Graviton families. */
9+
export const rdsGravitonReviewRule = createRule({
10+
id: RULE_ID,
11+
name: 'RDS DB Instance Without Graviton',
12+
description:
13+
'Flag RDS DB instances that still use non-Graviton instance families when a clear Graviton-based equivalent exists.',
14+
message: RULE_MESSAGE,
15+
provider: 'aws',
16+
service: RULE_SERVICE,
17+
supports: ['discovery'],
18+
discoveryDependencies: ['aws-rds-instances'],
19+
evaluateLive: ({ resources }) => {
20+
const findings = resources
21+
.get('aws-rds-instances')
22+
.filter(
23+
(instance) =>
24+
!isAwsRdsGravitonFamily(instance.instanceClass) &&
25+
shouldReviewAwsRdsInstanceClassForGraviton(instance.instanceClass),
26+
)
27+
.map((instance) => createFindingMatch(instance.dbInstanceIdentifier, instance.region, instance.accountId));
28+
29+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
30+
},
31+
});

0 commit comments

Comments
 (0)