Skip to content

Commit c5da62e

Browse files
feat: add IaC support for high-confidence AWS rules (#48)
* feat: add iac support for core ebs ec2 and rds rules * feat: add iac support for config-driven aws rules * feat: add iac support for dynamodb and elastic ip rules * feat: add iac support for eks and emr rules * feat: add iac support for route53 rules --------- Co-authored-by: Danny Steenman <15192660+dannysteenman@users.noreply.github.com>
1 parent 0a91238 commit c5da62e

Some content is hidden

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

50 files changed

+2930
-121
lines changed

.changeset/swift-parrots-float.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 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.

.changeset/tall-comics-invite.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 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.

docs/reference/rule-ids.md

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,62 +16,62 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`
1616

1717
| ID | Name | Service | Supports | Status |
1818
| --------------------- | ----------------------------------------- | ------- | -------------- | ----------- |
19-
| `CLDBRN-AWS-APIGATEWAY-1` | API Gateway Stage Caching Disabled | apigateway | discovery | Implemented |
20-
| `CLDBRN-AWS-CLOUDFRONT-1` | CloudFront Distribution Price Class All | cloudfront | discovery | Implemented |
19+
| `CLDBRN-AWS-APIGATEWAY-1` | API Gateway Stage Caching Disabled | apigateway | discovery, iac | Implemented |
20+
| `CLDBRN-AWS-CLOUDFRONT-1` | CloudFront Distribution Price Class All | cloudfront | discovery, iac | Implemented |
2121
| `CLDBRN-AWS-CLOUDFRONT-2` | CloudFront Distribution Unused | cloudfront | discovery | Implemented |
2222
| `CLDBRN-AWS-CLOUDTRAIL-1` | CloudTrail Redundant Global Trails | cloudtrail | discovery | Implemented |
2323
| `CLDBRN-AWS-CLOUDTRAIL-2` | CloudTrail Redundant Regional Trails | cloudtrail | discovery | Implemented |
24-
| `CLDBRN-AWS-CLOUDWATCH-1` | CloudWatch Log Group Missing Retention | cloudwatch | discovery | Implemented |
24+
| `CLDBRN-AWS-CLOUDWATCH-1` | CloudWatch Log Group Missing Retention | cloudwatch | discovery, iac | Implemented |
2525
| `CLDBRN-AWS-CLOUDWATCH-2` | CloudWatch Unused Log Streams | cloudwatch | discovery | Implemented |
2626
| `CLDBRN-AWS-CLOUDWATCH-3` | CloudWatch Log Group No Metric Filters | cloudwatch | discovery | Implemented |
2727
| `CLDBRN-AWS-COSTGUARDRAILS-1` | AWS Budgets Missing | costguardrails | discovery | Implemented |
2828
| `CLDBRN-AWS-COSTGUARDRAILS-2` | Cost Anomaly Detection Missing | costguardrails | discovery | Implemented |
2929
| `CLDBRN-AWS-COSTEXPLORER-1` | Cost Explorer Full Month Cost Changes | costexplorer | discovery | Implemented |
3030
| `CLDBRN-AWS-DYNAMODB-1` | DynamoDB Table Stale Data | dynamodb | discovery | Implemented |
31-
| `CLDBRN-AWS-DYNAMODB-2` | DynamoDB Table Without Autoscaling | dynamodb | discovery | Implemented |
31+
| `CLDBRN-AWS-DYNAMODB-2` | DynamoDB Table Without Autoscaling | dynamodb | discovery, iac | Implemented |
3232
| `CLDBRN-AWS-DYNAMODB-3` | DynamoDB Table Unused | dynamodb | discovery | Implemented |
3333
| `CLDBRN-AWS-EC2-1` | EC2 Instance Type Not Preferred | ec2 | iac, discovery | Implemented |
3434
| `CLDBRN-AWS-EC2-2` | S3 Interface VPC Endpoint Used | ec2 | iac | Implemented |
35-
| `CLDBRN-AWS-EC2-3` | Elastic IP Address Unassociated | ec2 | discovery | Implemented |
35+
| `CLDBRN-AWS-EC2-3` | Elastic IP Address Unassociated | ec2 | discovery, iac | Implemented |
3636
| `CLDBRN-AWS-EC2-4` | VPC Interface Endpoint Inactive | ec2 | discovery | Implemented |
3737
| `CLDBRN-AWS-EC2-5` | EC2 Instance Low Utilization | ec2 | discovery | Implemented |
38-
| `CLDBRN-AWS-EC2-6` | EC2 Instance Without Graviton | ec2 | discovery | Implemented |
38+
| `CLDBRN-AWS-EC2-6` | EC2 Instance Without Graviton | ec2 | discovery, iac | Implemented |
3939
| `CLDBRN-AWS-EC2-7` | EC2 Reserved Instance Expiring | ec2 | discovery | Implemented |
40-
| `CLDBRN-AWS-EC2-8` | EC2 Instance Large Size | ec2 | discovery | Implemented |
40+
| `CLDBRN-AWS-EC2-8` | EC2 Instance Large Size | ec2 | discovery, iac | Implemented |
4141
| `CLDBRN-AWS-EC2-9` | EC2 Instance Long Running | ec2 | discovery | Implemented |
4242
| `CLDBRN-AWS-ECS-1` | ECS Container Instance Without Graviton | ecs | discovery | Implemented |
4343
| `CLDBRN-AWS-ECS-2` | ECS Cluster Low CPU Utilization | ecs | discovery | Implemented |
4444
| `CLDBRN-AWS-ECS-3` | ECS Service Missing Autoscaling Policy | ecs | discovery | Implemented |
4545
| `CLDBRN-AWS-EBS-1` | EBS Volume Type Not Current Generation | ebs | discovery, iac | Implemented |
4646
| `CLDBRN-AWS-EBS-2` | EBS Volume Unattached | ebs | discovery | Implemented |
4747
| `CLDBRN-AWS-EBS-3` | EBS Volume Attached To Stopped Instances | ebs | discovery | Implemented |
48-
| `CLDBRN-AWS-EBS-4` | EBS Volume Large Size | ebs | discovery | Implemented |
49-
| `CLDBRN-AWS-EBS-5` | EBS Volume High Provisioned IOPS | ebs | discovery | Implemented |
50-
| `CLDBRN-AWS-EBS-6` | EBS Volume Low Provisioned IOPS On io1/io2 | ebs | discovery | Implemented |
48+
| `CLDBRN-AWS-EBS-4` | EBS Volume Large Size | ebs | discovery, iac | Implemented |
49+
| `CLDBRN-AWS-EBS-5` | EBS Volume High Provisioned IOPS | ebs | discovery, iac | Implemented |
50+
| `CLDBRN-AWS-EBS-6` | EBS Volume Low Provisioned IOPS On io1/io2 | ebs | discovery, iac | Implemented |
5151
| `CLDBRN-AWS-EBS-7` | EBS Snapshot Max Age Exceeded | ebs | discovery | Implemented |
5252
| `CLDBRN-AWS-ECR-1` | ECR Repository Missing Lifecycle Policy | ecr | iac, discovery | Implemented |
53-
| `CLDBRN-AWS-EKS-1` | EKS Node Group Without Graviton | eks | discovery | Implemented |
53+
| `CLDBRN-AWS-EKS-1` | EKS Node Group Without Graviton | eks | discovery, iac | Implemented |
5454
| `CLDBRN-AWS-ELASTICACHE-1` | ElastiCache Cluster Missing Reserved Coverage | elasticache | discovery | Implemented |
5555
| `CLDBRN-AWS-ELASTICACHE-2` | ElastiCache Cluster Idle | elasticache | discovery | Implemented |
5656
| `CLDBRN-AWS-ELB-1` | Application Load Balancer Without Targets | elb | discovery | Implemented |
5757
| `CLDBRN-AWS-ELB-2` | Classic Load Balancer Without Instances | elb | discovery | Implemented |
5858
| `CLDBRN-AWS-ELB-3` | Gateway Load Balancer Without Targets | elb | discovery | Implemented |
5959
| `CLDBRN-AWS-ELB-4` | Network Load Balancer Without Targets | elb | discovery | Implemented |
6060
| `CLDBRN-AWS-ELB-5` | Load Balancer Idle | elb | discovery | Implemented |
61-
| `CLDBRN-AWS-EMR-1` | EMR Cluster Previous Generation Instance Types | emr | discovery | Implemented |
61+
| `CLDBRN-AWS-EMR-1` | EMR Cluster Previous Generation Instance Types | emr | discovery, iac | Implemented |
6262
| `CLDBRN-AWS-EMR-2` | EMR Cluster Idle | emr | discovery | Implemented |
6363
| `CLDBRN-AWS-RDS-1` | RDS Instance Class Not Preferred | rds | iac, discovery | Implemented |
6464
| `CLDBRN-AWS-RDS-2` | RDS DB Instance Idle | rds | discovery | Implemented |
6565
| `CLDBRN-AWS-RDS-3` | RDS DB Instance Missing Reserved Coverage | rds | discovery | Implemented |
66-
| `CLDBRN-AWS-RDS-4` | RDS DB Instance Without Graviton | rds | discovery | Implemented |
66+
| `CLDBRN-AWS-RDS-4` | RDS DB Instance Without Graviton | rds | discovery, iac | Implemented |
6767
| `CLDBRN-AWS-RDS-5` | RDS DB Instance Low CPU Utilization | rds | discovery | Implemented |
68-
| `CLDBRN-AWS-RDS-6` | RDS DB Instance Unsupported Engine Version | rds | discovery | Implemented |
68+
| `CLDBRN-AWS-RDS-6` | RDS DB Instance Unsupported Engine Version | rds | discovery, iac | Implemented |
6969
| `CLDBRN-AWS-RDS-7` | RDS Snapshot Without Source DB Instance | rds | discovery | Implemented |
7070
| `CLDBRN-AWS-REDSHIFT-1` | Redshift Cluster Low CPU Utilization | redshift | discovery | Implemented |
7171
| `CLDBRN-AWS-REDSHIFT-2` | Redshift Cluster Missing Reserved Coverage | redshift | discovery | Implemented |
7272
| `CLDBRN-AWS-REDSHIFT-3` | Redshift Cluster Pause Resume Not Enabled | redshift | discovery | Implemented |
73-
| `CLDBRN-AWS-ROUTE53-1` | Route 53 Record Higher TTL | route53 | discovery | Implemented |
74-
| `CLDBRN-AWS-ROUTE53-2` | Route 53 Health Check Unused | route53 | discovery | Implemented |
73+
| `CLDBRN-AWS-ROUTE53-1` | Route 53 Record Higher TTL | route53 | discovery, iac | Implemented |
74+
| `CLDBRN-AWS-ROUTE53-2` | Route 53 Health Check Unused | route53 | discovery, iac | Implemented |
7575
| `CLDBRN-AWS-S3-1` | S3 Missing Lifecycle Configuration | s3 | iac, discovery | Implemented |
7676
| `CLDBRN-AWS-S3-2` | S3 Bucket Storage Class Not Optimized | s3 | iac, discovery | Implemented |
7777
| `CLDBRN-AWS-SECRETSMANAGER-1` | Secrets Manager Secret Unused | secretsmanager | discovery | Implemented |
@@ -164,7 +164,7 @@ Format: `CLDBRN-{PROVIDER}-{SERVICE}-{N}`
164164

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

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

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

packages/rules/src/aws/apigateway/caching-disabled.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ export const apiGatewayCachingDisabledRule = createRule({
1212
message: RULE_MESSAGE,
1313
provider: 'aws',
1414
service: RULE_SERVICE,
15-
supports: ['discovery'],
15+
supports: ['discovery', 'iac'],
1616
discoveryDependencies: ['aws-apigateway-stages'],
17+
staticDependencies: ['aws-apigateway-stages'],
1718
evaluateLive: ({ resources }) => {
1819
const findings = resources
1920
.get('aws-apigateway-stages')
@@ -22,4 +23,12 @@ export const apiGatewayCachingDisabledRule = createRule({
2223

2324
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
2425
},
26+
evaluateStatic: ({ resources }) => {
27+
const findings = resources
28+
.get('aws-apigateway-stages')
29+
.filter((stage) => stage.cacheClusterEnabled === false)
30+
.map((stage) => createFindingMatch(stage.resourceId, undefined, undefined, stage.location));
31+
32+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
33+
},
2534
});

packages/rules/src/aws/cloudfront/distribution-pricing-class.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ export const cloudFrontDistributionPricingClassRule = createRule({
1212
message: RULE_MESSAGE,
1313
provider: 'aws',
1414
service: RULE_SERVICE,
15-
supports: ['discovery'],
15+
supports: ['discovery', 'iac'],
1616
discoveryDependencies: ['aws-cloudfront-distributions'],
17+
staticDependencies: ['aws-cloudfront-distributions'],
1718
evaluateLive: ({ resources }) => {
1819
const findings = resources
1920
.get('aws-cloudfront-distributions')
@@ -24,4 +25,12 @@ export const cloudFrontDistributionPricingClassRule = createRule({
2425

2526
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
2627
},
28+
evaluateStatic: ({ resources }) => {
29+
const findings = resources
30+
.get('aws-cloudfront-distributions')
31+
.filter((distribution) => distribution.priceClass === 'PriceClass_All')
32+
.map((distribution) => createFindingMatch(distribution.resourceId, undefined, undefined, distribution.location));
33+
34+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
35+
},
2736
});

packages/rules/src/aws/cloudwatch/log-group-retention.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ const RULE_SERVICE = 'cloudwatch';
55
const RULE_MESSAGE =
66
'CloudWatch log groups should define a retention policy unless AWS manages lifecycle automatically.';
77

8+
const hasMissingRetention = (
9+
retentionInDays: number | null | undefined,
10+
logGroupClass: string | null | undefined,
11+
): boolean => retentionInDays === undefined && logGroupClass !== 'DELIVERY';
12+
813
/** Flag CloudWatch log groups that do not define retention and are not delivery-managed. */
914
export const cloudWatchLogGroupRetentionRule = createRule({
1015
id: RULE_ID,
@@ -13,14 +18,23 @@ export const cloudWatchLogGroupRetentionRule = createRule({
1318
message: RULE_MESSAGE,
1419
provider: 'aws',
1520
service: RULE_SERVICE,
16-
supports: ['discovery'],
21+
supports: ['discovery', 'iac'],
1722
discoveryDependencies: ['aws-cloudwatch-log-groups'],
23+
staticDependencies: ['aws-cloudwatch-log-groups'],
1824
evaluateLive: ({ resources }) => {
1925
const findings = resources
2026
.get('aws-cloudwatch-log-groups')
21-
.filter((logGroup) => logGroup.retentionInDays === undefined && logGroup.logGroupClass !== 'DELIVERY')
27+
.filter((logGroup) => hasMissingRetention(logGroup.retentionInDays, logGroup.logGroupClass))
2228
.map((logGroup) => createFindingMatch(logGroup.logGroupName, logGroup.region, logGroup.accountId));
2329

2430
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
2531
},
32+
evaluateStatic: ({ resources }) => {
33+
const findings = resources
34+
.get('aws-cloudwatch-log-groups')
35+
.filter((logGroup) => hasMissingRetention(logGroup.retentionInDays, logGroup.logGroupClass))
36+
.map((logGroup) => createFindingMatch(logGroup.resourceId, undefined, undefined, logGroup.location));
37+
38+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
39+
},
2640
});

packages/rules/src/aws/dynamodb/table-without-autoscaling.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ export const dynamoDbTableWithoutAutoscalingRule = createRule({
1515
message: RULE_MESSAGE,
1616
provider: 'aws',
1717
service: RULE_SERVICE,
18-
supports: ['discovery'],
18+
supports: ['discovery', 'iac'],
1919
discoveryDependencies: ['aws-dynamodb-tables', 'aws-dynamodb-autoscaling'],
20+
staticDependencies: ['aws-dynamodb-tables', 'aws-dynamodb-autoscaling'],
2021
evaluateLive: ({ resources }) => {
2122
const autoscalingByTable = new Map(
2223
resources
@@ -36,4 +37,22 @@ export const dynamoDbTableWithoutAutoscalingRule = createRule({
3637

3738
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
3839
},
40+
evaluateStatic: ({ resources }) => {
41+
const autoscalingByTable = new Map(
42+
resources
43+
.get('aws-dynamodb-autoscaling')
44+
.filter((table) => table.tableName !== null)
45+
.map((table) => [table.tableName, table] as const),
46+
);
47+
const findings = resources
48+
.get('aws-dynamodb-tables')
49+
.filter((table) => table.billingMode === 'PROVISIONED' && table.tableName !== null)
50+
.filter((table) => {
51+
const autoscaling = autoscalingByTable.get(table.tableName);
52+
return autoscaling ? !autoscaling.hasReadTarget && !autoscaling.hasWriteTarget : true;
53+
})
54+
.map((table) => createFindingMatch(table.resourceId, undefined, undefined, table.location));
55+
56+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
57+
},
3958
});

packages/rules/src/aws/ebs/high-iops-volume.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ const HIGH_IOPS_VOLUME_TYPES = new Set(['io1', 'io2']);
77
// Treat 32k IOPS as the threshold where provisioned io1/io2 volumes merit explicit review.
88
const HIGH_IOPS_THRESHOLD = 32000;
99

10+
const hasHighProvisionedIops = (volumeType: string | null | undefined, iops: number | null | undefined): boolean =>
11+
volumeType !== null &&
12+
volumeType !== undefined &&
13+
HIGH_IOPS_VOLUME_TYPES.has(volumeType) &&
14+
iops !== null &&
15+
iops !== undefined &&
16+
iops > HIGH_IOPS_THRESHOLD;
17+
1018
/** Flag io1 and io2 EBS volumes provisioned above the high-IOPS threshold. */
1119
export const ebsHighIopsVolumeRule = createRule({
1220
id: RULE_ID,
@@ -15,19 +23,23 @@ export const ebsHighIopsVolumeRule = createRule({
1523
message: RULE_MESSAGE,
1624
provider: 'aws',
1725
service: RULE_SERVICE,
18-
supports: ['discovery'],
26+
supports: ['discovery', 'iac'],
1927
discoveryDependencies: ['aws-ebs-volumes'],
28+
staticDependencies: ['aws-ebs-volumes'],
2029
evaluateLive: ({ resources }) => {
2130
const findings = resources
2231
.get('aws-ebs-volumes')
23-
.filter(
24-
(volume) =>
25-
HIGH_IOPS_VOLUME_TYPES.has(volume.volumeType) &&
26-
volume.iops !== undefined &&
27-
volume.iops > HIGH_IOPS_THRESHOLD,
28-
)
32+
.filter((volume) => hasHighProvisionedIops(volume.volumeType, volume.iops))
2933
.map((volume) => createFindingMatch(volume.volumeId, volume.region, volume.accountId));
3034

3135
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
3236
},
37+
evaluateStatic: ({ resources }) => {
38+
const findings = resources
39+
.get('aws-ebs-volumes')
40+
.filter((volume) => hasHighProvisionedIops(volume.volumeType, volume.iops))
41+
.map((volume) => createFindingMatch(volume.resourceId, undefined, undefined, volume.location));
42+
43+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
44+
},
3345
});

packages/rules/src/aws/ebs/large-volume.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ const RULE_MESSAGE = 'EBS volumes larger than 100 GiB should be reviewed.';
66
// Treat volumes above 100 GiB as oversized enough to warrant an explicit cost review.
77
const LARGE_VOLUME_SIZE_THRESHOLD_GIB = 100;
88

9+
const isLargeEbsVolume = (sizeGiB: number | null | undefined): boolean =>
10+
sizeGiB !== null && sizeGiB !== undefined && sizeGiB > LARGE_VOLUME_SIZE_THRESHOLD_GIB;
11+
912
/** Flag EBS volumes that exceed the large-volume review threshold. */
1013
export const ebsLargeVolumeRule = createRule({
1114
id: RULE_ID,
@@ -14,14 +17,23 @@ export const ebsLargeVolumeRule = createRule({
1417
message: RULE_MESSAGE,
1518
provider: 'aws',
1619
service: RULE_SERVICE,
17-
supports: ['discovery'],
20+
supports: ['discovery', 'iac'],
1821
discoveryDependencies: ['aws-ebs-volumes'],
22+
staticDependencies: ['aws-ebs-volumes'],
1923
evaluateLive: ({ resources }) => {
2024
const findings = resources
2125
.get('aws-ebs-volumes')
22-
.filter((volume) => volume.sizeGiB > LARGE_VOLUME_SIZE_THRESHOLD_GIB)
26+
.filter((volume) => isLargeEbsVolume(volume.sizeGiB))
2327
.map((volume) => createFindingMatch(volume.volumeId, volume.region, volume.accountId));
2428

2529
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'discovery', findings);
2630
},
31+
evaluateStatic: ({ resources }) => {
32+
const findings = resources
33+
.get('aws-ebs-volumes')
34+
.filter((volume) => isLargeEbsVolume(volume.sizeGiB))
35+
.map((volume) => createFindingMatch(volume.resourceId, undefined, undefined, volume.location));
36+
37+
return createFinding({ id: RULE_ID, service: RULE_SERVICE, message: RULE_MESSAGE }, 'iac', findings);
38+
},
2739
});

0 commit comments

Comments
 (0)