Skip to content

Commit 4d25e80

Browse files
feat(rules): add AWS discovery cleanup rules (#62)
* feat(rules): add AWS discovery cleanup rules * docs: format rule ids table * fix(sdk): handle stopped EC2 GMT times and idle SageMaker endpoints
1 parent a48376b commit 4d25e80

27 files changed

Lines changed: 1283 additions & 89 deletions

.changeset/bright-pears-burn.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 stopped EC2 instances, old manual RDS snapshots, and idle SageMaker endpoints.

.changeset/silent-mice-share.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 support for EC2 stop timestamps and SageMaker endpoint activity.

docs/architecture/sdk.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Current live-discovery behavior:
8383
- Resource Explorer inventory failures and dataset loader failures are fatal. The SDK does not degrade to partial live results.
8484
- Missing Lambda `Architectures` values from AWS are normalized to `['x86_64']`, matching the AWS default architecture.
8585
- Lambda hydrators limit in-flight `GetFunctionConfiguration` calls per region to avoid API throttling in large accounts.
86-
- 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:DescribeInstances`, `ec2:DescribeNatGateways`, `ec2:DescribeVolumes`, `eks:ListNodegroups`, `eks:DescribeNodegroup`, `lambda:GetFunctionConfiguration`, `rds:DescribeDBInstances`, `route53:ListHealthChecks`, `route53:ListHostedZones`, `route53:ListResourceRecordSets`, `s3:GetLifecycleConfiguration`, `s3:GetIntelligentTieringConfiguration`, `sagemaker:DescribeNotebookInstance`, and `secretsmanager:DescribeSecret`.
86+
- 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:DescribeInstances`, `ec2:DescribeNatGateways`, `ec2:DescribeVolumes`, `eks:ListNodegroups`, `eks:DescribeNodegroup`, `lambda:GetFunctionConfiguration`, `rds:DescribeDBInstances`, `route53:ListHealthChecks`, `route53:ListHostedZones`, `route53:ListResourceRecordSets`, `s3:GetLifecycleConfiguration`, `s3:GetIntelligentTieringConfiguration`, `sagemaker:DescribeEndpoint`, `sagemaker:DescribeEndpointConfig`, `sagemaker:DescribeNotebookInstance`, and `secretsmanager:DescribeSecret`.
8787

8888
## Public Result Shape
8989

docs/reference/rule-ids.md

Lines changed: 82 additions & 79 deletions
Large diffs are not rendered by default.

packages/rules/src/aws/ec2/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ec2PreferredInstanceTypeRule } from './preferred-instance-types.js';
99
import { ec2ReservedInstanceExpiringRule } from './reserved-instance-expiring.js';
1010
import { ec2ReservedInstanceRecentlyExpiredRule } from './reserved-instance-recently-expired.js';
1111
import { ec2S3InterfaceEndpointRule } from './s3-interface-endpoint.js';
12+
import { ec2StoppedInstanceRule } from './stopped-instance.js';
1213
import { ec2UnassociatedElasticIpRule } from './unassociated-elastic-ip.js';
1314

1415
/** Aggregate AWS EC2 rule definitions. */
@@ -25,4 +26,5 @@ export const ec2Rules = [
2526
ec2DetailedMonitoringEnabledRule,
2627
ec2IdleNatGatewayRule,
2728
ec2ReservedInstanceRecentlyExpiredRule,
29+
ec2StoppedInstanceRule,
2830
];
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';
2+
3+
const RULE_ID = 'CLDBRN-AWS-EC2-13';
4+
const RULE_SERVICE = 'ec2';
5+
const RULE_MESSAGE = 'Stopped EC2 instances with a parsed stop time older than 30 days should be reviewed for cleanup.';
6+
7+
const DAY_MS = 24 * 60 * 60 * 1000;
8+
const STOPPED_INSTANCE_MAX_AGE_DAYS = 30;
9+
10+
/** Flag stopped EC2 instances whose parsed stop time is older than 30 days. */
11+
export const ec2StoppedInstanceRule = createRule({
12+
id: RULE_ID,
13+
name: 'EC2 Instance Stopped',
14+
description: 'Flag stopped EC2 instances whose parsed stop time is at least 30 days old.',
15+
message: RULE_MESSAGE,
16+
provider: 'aws',
17+
service: RULE_SERVICE,
18+
supports: ['discovery'],
19+
discoveryDependencies: ['aws-ec2-instances'],
20+
evaluateLive: ({ resources }) => {
21+
const cutoff = Date.now() - STOPPED_INSTANCE_MAX_AGE_DAYS * DAY_MS;
22+
const findings = resources
23+
.get('aws-ec2-instances')
24+
.filter((instance) => {
25+
if (instance.state !== 'stopped' || !instance.stoppedAt) {
26+
return false;
27+
}
28+
29+
const stoppedAt = Date.parse(instance.stoppedAt);
30+
31+
return Number.isFinite(stoppedAt) && stoppedAt <= cutoff;
32+
})
33+
.map((instance) => createFindingMatch(instance.instanceId, instance.region, instance.accountId));
34+
35+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
36+
},
37+
});

packages/rules/src/aws/rds/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { rdsGravitonReviewRule } from './graviton-review.js';
22
import { rdsIdleInstanceRule } from './idle-instance.js';
33
import { rdsLowCpuUtilizationRule } from './low-cpu-utilization.js';
4+
import { rdsManualSnapshotMaxAgeRule } from './manual-snapshot-max-age.js';
45
import { rdsPerformanceInsightsExtendedRetentionRule } from './performance-insights-extended-retention.js';
56
import { rdsPreferredInstanceClassRule } from './preferred-instance-classes.js';
67
import { rdsReservedCoverageRule } from './reserved-coverage.js';
@@ -20,4 +21,5 @@ export const rdsRules = [
2021
rdsUnusedSnapshotsRule,
2122
rdsPerformanceInsightsExtendedRetentionRule,
2223
rdsStoppedInstanceRule,
24+
rdsManualSnapshotMaxAgeRule,
2325
];
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';
2+
3+
const RULE_ID = 'CLDBRN-AWS-RDS-10';
4+
const RULE_SERVICE = 'rds';
5+
const RULE_MESSAGE = 'Manual RDS snapshots older than 90 days should be reviewed for cleanup.';
6+
7+
const DAY_MS = 24 * 60 * 60 * 1000;
8+
const SNAPSHOT_MAX_AGE_DAYS = 90;
9+
10+
/** Flag manual RDS snapshots older than 90 days. */
11+
export const rdsManualSnapshotMaxAgeRule = createRule({
12+
id: RULE_ID,
13+
name: 'RDS Manual Snapshot Max Age Exceeded',
14+
description: 'Flag manual RDS snapshots older than 90 days.',
15+
message: RULE_MESSAGE,
16+
provider: 'aws',
17+
service: RULE_SERVICE,
18+
supports: ['discovery'],
19+
discoveryDependencies: ['aws-rds-snapshots'],
20+
evaluateLive: ({ resources }) => {
21+
const cutoff = Date.now() - SNAPSHOT_MAX_AGE_DAYS * DAY_MS;
22+
const findings = resources
23+
.get('aws-rds-snapshots')
24+
.filter((snapshot) => {
25+
if (snapshot.snapshotType !== 'manual' || !snapshot.snapshotCreateTime) {
26+
return false;
27+
}
28+
29+
const snapshotCreateTime = Date.parse(snapshot.snapshotCreateTime);
30+
31+
return Number.isFinite(snapshotCreateTime) && snapshotCreateTime <= cutoff;
32+
})
33+
.map((snapshot) => createFindingMatch(snapshot.dbSnapshotIdentifier, snapshot.region, snapshot.accountId));
34+
35+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
36+
},
37+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js';
2+
3+
const RULE_ID = 'CLDBRN-AWS-SAGEMAKER-2';
4+
const RULE_SERVICE = 'sagemaker';
5+
const RULE_MESSAGE =
6+
'SageMaker endpoints in service with zero invocations over 14 days should be reviewed for cleanup.';
7+
8+
const DAY_MS = 24 * 60 * 60 * 1000;
9+
const ENDPOINT_IDLE_WINDOW_DAYS = 14;
10+
11+
/** Flag SageMaker endpoints that are in service, old enough, and idle for 14 days. */
12+
export const sagemakerIdleEndpointRule = createRule({
13+
id: RULE_ID,
14+
name: 'SageMaker Endpoint Idle',
15+
description: 'Flag SageMaker endpoints in service whose 14-day invocation total is zero.',
16+
message: RULE_MESSAGE,
17+
provider: 'aws',
18+
service: RULE_SERVICE,
19+
supports: ['discovery'],
20+
discoveryDependencies: ['aws-sagemaker-endpoint-activity'],
21+
evaluateLive: ({ resources }) => {
22+
const cutoff = Date.now() - ENDPOINT_IDLE_WINDOW_DAYS * DAY_MS;
23+
const findings = resources
24+
.get('aws-sagemaker-endpoint-activity')
25+
.filter((endpoint) => {
26+
if (
27+
endpoint.endpointStatus !== 'InService' ||
28+
endpoint.totalInvocationsLast14Days !== 0 ||
29+
!endpoint.creationTime
30+
) {
31+
return false;
32+
}
33+
34+
const creationTime = Date.parse(endpoint.creationTime);
35+
36+
return Number.isFinite(creationTime) && creationTime <= cutoff;
37+
})
38+
.map((endpoint) => createFindingMatch(endpoint.endpointName, endpoint.region, endpoint.accountId));
39+
40+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
41+
},
42+
});
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1+
import { sagemakerIdleEndpointRule } from './idle-endpoint.js';
12
import { sagemakerRunningNotebookInstanceRule } from './running-notebook-instance.js';
23

34
/** Aggregate AWS SageMaker rule definitions. */
4-
export const sagemakerRules = [sagemakerRunningNotebookInstanceRule];
5+
export const sagemakerRules = [sagemakerRunningNotebookInstanceRule, sagemakerIdleEndpointRule];

0 commit comments

Comments
 (0)