diff --git a/.changeset/clean-cost-signals.md b/.changeset/clean-cost-signals.md new file mode 100644 index 0000000..c196839 --- /dev/null +++ b/.changeset/clean-cost-signals.md @@ -0,0 +1,7 @@ +--- +'@cloudburn/rules': minor +'@cloudburn/sdk': minor +cloudburn: minor +--- + +Remove four heuristic AWS rules that could flag valid cost choices without usage evidence: disabled API Gateway caching, missing CloudWatch metric filters, configured Lambda provisioned concurrency, and ungated S3 Intelligent-Tiering. Default scans no longer report those findings, and the unused provider datasets are no longer loaded or exported. diff --git a/docs/architecture/sdk.md b/docs/architecture/sdk.md index ac8f232..d414e96 100644 --- a/docs/architecture/sdk.md +++ b/docs/architecture/sdk.md @@ -110,7 +110,7 @@ Current live-discovery behavior: - Global tagging discovery is opt-in because it requires an accessible aggregator, and uses one paginated `ListResources` filter query (`resourcetype.supports:tags tag:none`) instead of per-service tagging APIs. - Missing Lambda `Architectures` values from AWS are normalized to `['x86_64']`, matching the AWS default architecture. - Lambda hydrators limit in-flight `GetFunctionConfiguration` calls per region to avoid API throttling in large accounts. -- 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`. +- Live scans require Resource Explorer access plus narrow hydrator permissions such as `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`. ## Public Result Shape diff --git a/docs/reference/rule-ids.md b/docs/reference/rule-ids.md index 72a21a9..a8facef 100644 --- a/docs/reference/rule-ids.md +++ b/docs/reference/rule-ids.md @@ -39,14 +39,12 @@ meaningful optimization opportunities, and `low` covers hygiene and smaller accu | ID | Severity | Description | Service | Supports | | ----------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | -------------- | -| `CLDBRN-AWS-APIGATEWAY-1` | medium | Flags REST API stages when `cacheClusterEnabled` is not explicitly `true`. | apigateway | discovery, iac | | `CLDBRN-AWS-CLOUDFRONT-1` | medium | Reviews only distributions using `PriceClass_All`. | cloudfront | discovery, iac | | `CLDBRN-AWS-CLOUDFRONT-2` | medium | Requires a complete 30-day `Requests` history and flags only distributions whose total request count stays below `100`. | cloudfront | discovery | | `CLDBRN-AWS-CLOUDTRAIL-1` | medium | Flag redundant multi-region CloudTrail trails when more than one trail covers the same account. | cloudtrail | discovery | | `CLDBRN-AWS-CLOUDTRAIL-2` | medium | Flag redundant single-region CloudTrail trails when more than one trail covers the same region. | cloudtrail | discovery | | `CLDBRN-AWS-CLOUDWATCH-1` | low | Flag CloudWatch log groups that do not define retention and are not delivery-managed. | cloudwatch | discovery, iac | | `CLDBRN-AWS-CLOUDWATCH-2` | low | Flags log groups whose most recent observed stream activity is missing or older than 90 days. Delivery-managed log groups remain exempt. | cloudwatch | discovery | -| `CLDBRN-AWS-CLOUDWATCH-3` | low | Reviews only log groups storing at least `1 GiB` and flags them when no metric filters are configured. | cloudwatch | discovery | | `CLDBRN-AWS-COSTGUARDRAILS-1` | low | Flags accounts whose AWS Budgets summary reports zero configured budgets. | costguardrails | discovery | | `CLDBRN-AWS-COSTGUARDRAILS-2` | low | Flags accounts whose Cost Anomaly Detection summary reports zero anomaly monitors. | costguardrails | discovery | | `CLDBRN-AWS-COSTGUARDRAILS-3` | high | Flags configured AWS Budgets only when normalized actual spend is strictly greater than the same-unit budget limit. Malformed and unit-mismatched spend details are skipped. | costguardrails | discovery | @@ -114,7 +112,6 @@ meaningful optimization opportunities, and `low` covers hygiene and smaller accu | `CLDBRN-AWS-S3-2` | medium | Recommend Intelligent-Tiering or another explicit storage-class transition for lifecycle-managed buckets. | s3 | iac, discovery | | `CLDBRN-AWS-S3-3` | low | Flags buckets when no enabled lifecycle rule aborts incomplete multipart uploads within 7 days. | s3 | iac, discovery | | `CLDBRN-AWS-S3-4` | medium | Flags only versioned buckets and requires either noncurrent-version expiration or transition cleanup to avoid unbounded version growth. | s3 | iac | -| `CLDBRN-AWS-S3-5` | low | Flags buckets with no lifecycle configuration and no enabled Intelligent-Tiering configuration; lifecycle-managed buckets stay with `CLDBRN-AWS-S3-2`, and an Intelligent-Tiering configuration counts as adoption intent. | s3 | iac, discovery | | `CLDBRN-AWS-SAGEMAKER-1` | high | Flags only notebook instances whose normalized status remains `InService`. | sagemaker | discovery | | `CLDBRN-AWS-SAGEMAKER-2` | high | Flags only endpoints whose normalized status remains `InService`, whose parsed `creationTime` is at least `14` days old, and whose complete 14-day `Invocations` total stays at `0`. Endpoints with incomplete metrics are skipped. | sagemaker | discovery | | `CLDBRN-AWS-SECRETSMANAGER-1` | low | Flags secrets with no `lastAccessedDate` and secrets whose parsed last access is at least `90` days old. | secretsmanager | discovery | @@ -123,7 +120,6 @@ meaningful optimization opportunities, and `low` covers hygiene and smaller accu | `CLDBRN-AWS-LAMBDA-2` | low | Uses 7-day CloudWatch totals and flags only functions whose observed `Errors / Invocations` ratio is greater than `10%`. | lambda | discovery | | `CLDBRN-AWS-LAMBDA-3` | low | 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. | lambda | discovery | | `CLDBRN-AWS-LAMBDA-4` | medium | 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. | lambda | discovery | -| `CLDBRN-AWS-LAMBDA-5` | high | Flags explicit provisioned concurrency configuration when provisioned concurrent executions are greater than zero. | lambda | iac | ## Presets diff --git a/packages/cloudburn/test/formatters.test.ts b/packages/cloudburn/test/formatters.test.ts index 7b29a46..e16ab3b 100644 --- a/packages/cloudburn/test/formatters.test.ts +++ b/packages/cloudburn/test/formatters.test.ts @@ -56,11 +56,10 @@ const resultWithLocation = { const resultWithSkippedRuleDiagnostic = { diagnostics: [ { - details: - 'Amazon CloudWatch Logs DescribeMetricFilters failed in us-east-1 with ThrottlingException: Rate exceeded.', - message: 'Skipped rule CLDBRN-AWS-CLOUDWATCH-3 because required discovery datasets were unavailable.', + details: 'Amazon CloudWatch Logs DescribeLogStreams failed in us-east-1 with ThrottlingException: Rate exceeded.', + message: 'Skipped rule CLDBRN-AWS-CLOUDWATCH-2 because required discovery datasets were unavailable.', provider: 'aws' as const, - ruleId: 'CLDBRN-AWS-CLOUDWATCH-3', + ruleId: 'CLDBRN-AWS-CLOUDWATCH-2', service: 'cloudwatch', source: 'discovery' as const, status: 'skipped' as const, @@ -149,8 +148,8 @@ describe('renderResponse', () => { expect(output).toContain('Diagnostics'); expect(output).toContain('Status'); - expect(output).toContain('CLDBRN-AWS-CLOUDWATCH-3'); - expect(output).toContain('Skipped rule CLDBRN-AWS-CLOUDWATCH-3'); + expect(output).toContain('CLDBRN-AWS-CLOUDWATCH-2'); + expect(output).toContain('Skipped rule CLDBRN-AWS-CLOUDWATCH-2'); expect(output).not.toContain('ResourceId'); expect(output).not.toContain('AccountId'); }); diff --git a/packages/rules/src/aws/apigateway/caching-disabled.ts b/packages/rules/src/aws/apigateway/caching-disabled.ts deleted file mode 100644 index 2cf5321..0000000 --- a/packages/rules/src/aws/apigateway/caching-disabled.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js'; - -const RULE_ID = 'CLDBRN-AWS-APIGATEWAY-1'; -const RULE_SERVICE = 'apigateway'; -const RULE_SEVERITY = 'medium' as const; -const RULE_MESSAGE = 'API Gateway REST API stages should enable caching when stage caching is available.'; - -/** Flag API Gateway REST API stages whose cache cluster is disabled. */ -export const apiGatewayCachingDisabledRule = createRule({ - severity: RULE_SEVERITY, - id: RULE_ID, - name: 'API Gateway Stage Caching Disabled', - description: 'Flag API Gateway REST API stages with caching disabled.', - message: RULE_MESSAGE, - provider: 'aws', - service: RULE_SERVICE, - supports: ['discovery', 'iac'], - discoveryDependencies: ['aws-apigateway-stages'], - staticDependencies: ['aws-apigateway-stages'], - evaluateLive: ({ resources }) => { - const findings = resources - .get('aws-apigateway-stages') - .filter((stage) => stage.cacheClusterEnabled !== true) - .map((stage) => createFindingMatch(stage.stageArn, stage.region, stage.accountId)); - - return createFinding( - { id: RULE_ID, service: RULE_SERVICE, severity: RULE_SEVERITY, message: RULE_MESSAGE }, - 'discovery', - findings, - ); - }, - evaluateStatic: ({ resources }) => { - const findings = resources - .get('aws-apigateway-stages') - .filter((stage) => stage.cacheClusterEnabled === false) - .map((stage) => createFindingMatch(stage.resourceId, undefined, undefined, stage.location)); - - return createFinding( - { id: RULE_ID, service: RULE_SERVICE, severity: RULE_SEVERITY, message: RULE_MESSAGE }, - 'iac', - findings, - ); - }, -}); diff --git a/packages/rules/src/aws/apigateway/index.ts b/packages/rules/src/aws/apigateway/index.ts deleted file mode 100644 index e5d2853..0000000 --- a/packages/rules/src/aws/apigateway/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { apiGatewayCachingDisabledRule } from './caching-disabled.js'; - -// Intent: aggregate AWS API Gateway rule definitions. -export const apigatewayRules = [apiGatewayCachingDisabledRule]; diff --git a/packages/rules/src/aws/cloudwatch/index.ts b/packages/rules/src/aws/cloudwatch/index.ts index 67ab21f..62c9787 100644 --- a/packages/rules/src/aws/cloudwatch/index.ts +++ b/packages/rules/src/aws/cloudwatch/index.ts @@ -1,10 +1,5 @@ -import { cloudWatchLogGroupNoMetricFiltersRule } from './log-group-no-metric-filters.js'; import { cloudWatchLogGroupRetentionRule } from './log-group-retention.js'; import { cloudWatchUnusedLogStreamsRule } from './unused-log-streams.js'; /** Aggregate AWS CloudWatch rule definitions. */ -export const cloudwatchRules = [ - cloudWatchLogGroupRetentionRule, - cloudWatchUnusedLogStreamsRule, - cloudWatchLogGroupNoMetricFiltersRule, -]; +export const cloudwatchRules = [cloudWatchLogGroupRetentionRule, cloudWatchUnusedLogStreamsRule]; diff --git a/packages/rules/src/aws/cloudwatch/log-group-no-metric-filters.ts b/packages/rules/src/aws/cloudwatch/log-group-no-metric-filters.ts deleted file mode 100644 index 58ff1c2..0000000 --- a/packages/rules/src/aws/cloudwatch/log-group-no-metric-filters.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js'; - -const RULE_ID = 'CLDBRN-AWS-CLOUDWATCH-3'; -const RULE_SERVICE = 'cloudwatch'; -const RULE_SEVERITY = 'low' as const; -const RULE_MESSAGE = - 'CloudWatch log groups storing at least 1 GB should define metric filters or reduce retention aggressively.'; -const MIN_STORED_BYTES = 1_073_741_824; -const getCoverageKey = (accountId: string, region: string, logGroupName: string): string => - `${accountId}:${region}:${logGroupName}`; - -/** Flag large CloudWatch log groups that have no metric filters configured. */ -export const cloudWatchLogGroupNoMetricFiltersRule = createRule({ - severity: RULE_SEVERITY, - id: RULE_ID, - name: 'CloudWatch Log Group No Metric Filters', - description: 'Flag CloudWatch log groups storing at least 1 GB when they define no metric filters.', - message: RULE_MESSAGE, - provider: 'aws', - service: RULE_SERVICE, - supports: ['discovery'], - discoveryDependencies: ['aws-cloudwatch-log-groups', 'aws-cloudwatch-log-metric-filter-coverage'], - evaluateLive: ({ resources }) => { - const coverageByLogGroupKey = new Map( - resources - .get('aws-cloudwatch-log-metric-filter-coverage') - .map( - (coverage) => [getCoverageKey(coverage.accountId, coverage.region, coverage.logGroupName), coverage] as const, - ), - ); - - const findings = resources - .get('aws-cloudwatch-log-groups') - .filter((logGroup) => (logGroup.storedBytes ?? 0) >= MIN_STORED_BYTES) - .filter((logGroup) => { - const coverage = coverageByLogGroupKey.get( - getCoverageKey(logGroup.accountId, logGroup.region, logGroup.logGroupName), - ); - - return coverage?.metricFilterCount === 0; - }) - .map((logGroup) => createFindingMatch(logGroup.logGroupName, logGroup.region, logGroup.accountId)); - - return createFinding( - { id: RULE_ID, service: RULE_SERVICE, severity: RULE_SEVERITY, message: RULE_MESSAGE }, - 'discovery', - findings, - ); - }, -}); diff --git a/packages/rules/src/aws/index.ts b/packages/rules/src/aws/index.ts index fc77f25..c0c69ff 100644 --- a/packages/rules/src/aws/index.ts +++ b/packages/rules/src/aws/index.ts @@ -1,4 +1,3 @@ -import { apigatewayRules } from './apigateway/index.js'; import { cloudfrontRules } from './cloudfront/index.js'; import { cloudtrailRules } from './cloudtrail/index.js'; import { cloudwatchRules } from './cloudwatch/index.js'; @@ -25,7 +24,6 @@ import { taggingRules } from './tagging/index.js'; // Intent: aggregate all AWS rules into a single provider collection. // TODO(cloudburn): keep this list synchronized as new AWS rules are added. export const awsRules = [ - ...apigatewayRules, ...cloudfrontRules, ...cloudtrailRules, ...cloudwatchRules, diff --git a/packages/rules/src/aws/lambda/index.ts b/packages/rules/src/aws/lambda/index.ts index eb6f73c..4583455 100644 --- a/packages/rules/src/aws/lambda/index.ts +++ b/packages/rules/src/aws/lambda/index.ts @@ -2,7 +2,6 @@ import { lambdaCostOptimalArchitectureRule } from './cost-optimal-architecture.j import { lambdaExcessiveTimeoutRule } from './excessive-timeout.js'; import { lambdaHighErrorRateRule } from './high-error-rate.js'; import { lambdaMemoryOverprovisioningRule } from './memory-overprovisioning.js'; -import { lambdaProvisionedConcurrencyConfiguredRule } from './provisioned-concurrency-configured.js'; // Intent: aggregate AWS Lambda rule definitions. export const lambdaRules = [ @@ -10,5 +9,4 @@ export const lambdaRules = [ lambdaHighErrorRateRule, lambdaExcessiveTimeoutRule, lambdaMemoryOverprovisioningRule, - lambdaProvisionedConcurrencyConfiguredRule, ]; diff --git a/packages/rules/src/aws/lambda/provisioned-concurrency-configured.ts b/packages/rules/src/aws/lambda/provisioned-concurrency-configured.ts deleted file mode 100644 index 7994124..0000000 --- a/packages/rules/src/aws/lambda/provisioned-concurrency-configured.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createFinding, createFindingMatch, createRule } from '../../shared/helpers.js'; - -const RULE_ID = 'CLDBRN-AWS-LAMBDA-5'; -const RULE_SERVICE = 'lambda'; -const RULE_SEVERITY = 'high' as const; -const RULE_MESSAGE = 'Lambda provisioned concurrency should be reviewed for steady low-latency demand.'; - -/** Flag explicit Lambda provisioned concurrency configuration. */ -export const lambdaProvisionedConcurrencyConfiguredRule = createRule({ - severity: RULE_SEVERITY, - id: RULE_ID, - name: 'Lambda Provisioned Concurrency Configured', - description: 'Flag explicit Lambda provisioned concurrency configuration for cost review.', - message: RULE_MESSAGE, - provider: 'aws', - service: RULE_SERVICE, - supports: ['iac'], - staticDependencies: ['aws-lambda-provisioned-concurrency'], - evaluateStatic: ({ resources }) => { - const findings = resources - .get('aws-lambda-provisioned-concurrency') - .filter((config) => config.provisionedConcurrentExecutions !== null && config.provisionedConcurrentExecutions > 0) - .map((config) => createFindingMatch(config.resourceId, undefined, undefined, config.location)); - - return createFinding( - { id: RULE_ID, service: RULE_SERVICE, severity: RULE_SEVERITY, message: RULE_MESSAGE }, - 'iac', - findings, - ); - }, -}); diff --git a/packages/rules/src/aws/s3/index.ts b/packages/rules/src/aws/s3/index.ts index 34b4972..c9ff1ea 100644 --- a/packages/rules/src/aws/s3/index.ts +++ b/packages/rules/src/aws/s3/index.ts @@ -1,5 +1,4 @@ import { s3IncompleteMultipartUploadAbortRule } from './incomplete-multipart-upload-abort.js'; -import { s3IntelligentTieringRecommendationRule } from './intelligent-tiering-recommendation.js'; import { s3MissingLifecycleConfigRule } from './missing-lifecycle-config.js'; import { s3StorageClassOptimizationRule } from './storage-class-optimization.js'; import { s3VersionedBucketNoncurrentVersionCleanupRule } from './versioned-bucket-noncurrent-version-cleanup.js'; @@ -10,5 +9,4 @@ export const s3Rules = [ s3StorageClassOptimizationRule, s3IncompleteMultipartUploadAbortRule, s3VersionedBucketNoncurrentVersionCleanupRule, - s3IntelligentTieringRecommendationRule, ]; diff --git a/packages/rules/src/aws/s3/intelligent-tiering-recommendation.ts b/packages/rules/src/aws/s3/intelligent-tiering-recommendation.ts deleted file mode 100644 index 2e1beab..0000000 --- a/packages/rules/src/aws/s3/intelligent-tiering-recommendation.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { createFinding, createRule } from '../../shared/helpers.js'; -import { - createLiveS3BucketFindingMatch, - createStaticS3BucketFindingMatch, - shouldRecommendIntelligentTiering, -} from './shared.js'; - -const RULE_ID = 'CLDBRN-AWS-S3-5'; -const RULE_SERVICE = 's3'; -const RULE_SEVERITY = 'low' as const; -const RULE_MESSAGE = - 'S3 buckets without any storage-class transition should enable Intelligent-Tiering when access patterns are unknown.'; - -/** Flag S3 buckets with no lifecycle configuration and no Intelligent-Tiering configuration. */ -export const s3IntelligentTieringRecommendationRule = createRule({ - severity: RULE_SEVERITY, - id: RULE_ID, - name: 'S3 Bucket Without Intelligent-Tiering', - description: - 'Recommend Intelligent-Tiering for buckets that declare no lifecycle configuration and no Intelligent-Tiering configuration.', - message: RULE_MESSAGE, - provider: 'aws', - service: RULE_SERVICE, - supports: ['iac', 'discovery'], - discoveryDependencies: ['aws-s3-bucket-analyses'], - staticDependencies: ['aws-s3-bucket-analyses'], - evaluateLive: ({ resources }) => { - const findings = resources - .get('aws-s3-bucket-analyses') - .filter((bucket) => shouldRecommendIntelligentTiering(bucket)) - .map((bucket) => createLiveS3BucketFindingMatch(bucket)); - - return createFinding( - { id: RULE_ID, service: RULE_SERVICE, severity: RULE_SEVERITY, message: RULE_MESSAGE }, - 'discovery', - findings, - ); - }, - evaluateStatic: ({ resources }) => { - const findings = resources - .get('aws-s3-bucket-analyses') - .filter((bucket) => shouldRecommendIntelligentTiering(bucket)) - .map((bucket) => createStaticS3BucketFindingMatch(bucket)); - - return createFinding( - { id: RULE_ID, service: RULE_SERVICE, severity: RULE_SEVERITY, message: RULE_MESSAGE }, - 'iac', - findings, - ); - }, -}); diff --git a/packages/rules/src/aws/s3/shared.ts b/packages/rules/src/aws/s3/shared.ts index 24a52e1..aa88af5 100644 --- a/packages/rules/src/aws/s3/shared.ts +++ b/packages/rules/src/aws/s3/shared.ts @@ -21,23 +21,6 @@ export const hasMissingStorageClassOptimization = (bucket: AwsS3BucketAnalysisFl !bucket.hasIntelligentTieringTransition && !bucket.hasAlternativeStorageClassTransition; -/** - * Returns whether an S3 bucket should be flagged for an Intelligent-Tiering recommendation. - * - * Buckets with any lifecycle signal stay with `CLDBRN-AWS-S3-2`, which reviews the storage-class - * choice of lifecycle-managed buckets. This keeps the two storage-class rules disjoint. - * - * An Intelligent-Tiering configuration is treated as adoption intent, not as proof of tiering. AWS - * only uses that configuration to move objects already stored in the Intelligent-Tiering storage - * class into the Archive Access tiers, and objects enter the storage class through their upload - * storage class or a lifecycle transition. Neither signal is visible per object in the bucket-level - * datasets, so a bucket that declares an archive-tier configuration while still storing Standard - * objects stays unflagged. Skipping keeps the rule quiet for owners who did adopt the storage class - * directly, at the cost of that gap. - */ -export const shouldRecommendIntelligentTiering = (bucket: AwsS3BucketAnalysisFlags): boolean => - !bucket.hasLifecycleSignal && !bucket.hasIntelligentTieringConfiguration; - /** Returns whether a versioned S3 bucket should be flagged for missing noncurrent-version cleanup. */ export const hasMissingNoncurrentVersionCleanup = ( bucket: Pick, diff --git a/packages/rules/src/index.ts b/packages/rules/src/index.ts index e315123..45d0c17 100644 --- a/packages/rules/src/index.ts +++ b/packages/rules/src/index.ts @@ -13,13 +13,11 @@ export { toRuleIds, } from './shared/helpers.js'; export type { - AwsApiGatewayStage, AwsCloudFrontDistribution, AwsCloudFrontDistributionRequestActivity, AwsCloudTrailTrail, AwsCloudWatchLogGroup, AwsCloudWatchLogGroupRecentStreamActivity, - AwsCloudWatchLogMetricFilterCoverage, AwsCloudWatchLogStream, AwsCostAnomalyMonitor, AwsCostGuardrailBudget, @@ -72,7 +70,6 @@ export type { AwsSageMakerEndpointActivity, AwsSageMakerNotebookInstance, AwsSecretsManagerSecret, - AwsStaticApiGatewayStage, AwsStaticCloudFrontDistribution, AwsStaticCloudWatchLogGroup, AwsStaticDynamoDbAutoscaling, @@ -88,7 +85,6 @@ export type { AwsStaticElastiCacheCluster, AwsStaticEmrCluster, AwsStaticLambdaFunction, - AwsStaticLambdaProvisionedConcurrency, AwsStaticRdsInstance, AwsStaticRedshiftCluster, AwsStaticRoute53HealthCheck, diff --git a/packages/rules/src/shared/metadata.ts b/packages/rules/src/shared/metadata.ts index 595d7b6..7f3b6cb 100644 --- a/packages/rules/src/shared/metadata.ts +++ b/packages/rules/src/shared/metadata.ts @@ -75,17 +75,6 @@ export type AwsCloudTrailTrail = { accountId: string; }; -/** Discovered API Gateway REST API stage normalized for cache review checks. */ -export type AwsApiGatewayStage = { - stageArn: string; - restApiId: string; - stageName: string; - /** REST API stage caching is explicitly configured through the cache cluster flag. */ - cacheClusterEnabled?: boolean; - region: string; - accountId: string; -}; - /** Discovered CloudWatch Logs log group normalized for retention checks. */ export type AwsCloudWatchLogGroup = { logGroupArn: string; @@ -123,14 +112,6 @@ export type AwsCloudWatchLogGroupRecentStreamActivity = { accountId: string; }; -/** Discovered CloudWatch Logs metric-filter coverage keyed by log group. */ -export type AwsCloudWatchLogMetricFilterCoverage = { - logGroupName: string; - metricFilterCount: number; - region: string; - accountId: string; -}; - /** Discovered CloudFront distribution normalized for price-class review checks. */ export type AwsCloudFrontDistribution = { distributionArn: string; @@ -687,7 +668,6 @@ export type AwsDiscoveryCatalog = { /** Rule-facing live discovery dataset key exposed through the evaluation context. */ export type SharedDatasetKey = - | 'aws-apigateway-stages' | 'aws-cloudfront-distributions' | 'aws-cloudwatch-log-groups' | 'aws-dynamodb-autoscaling' @@ -710,13 +690,11 @@ export type SharedDatasetKey = /** Rule-facing live discovery dataset key exposed through the evaluation context. */ export type DiscoveryDatasetKey = - | 'aws-apigateway-stages' | 'aws-cloudtrail-trails' | 'aws-cloudfront-distributions' | 'aws-cloudfront-distribution-request-activity' | 'aws-cloudwatch-log-groups' | 'aws-cloudwatch-log-group-recent-stream-activity' - | 'aws-cloudwatch-log-metric-filter-coverage' | 'aws-cloudwatch-log-streams' | 'aws-cost-usage' | 'aws-cost-anomaly-monitors' @@ -768,13 +746,11 @@ export type DiscoveryDatasetKey = /** Normalized live discovery datasets available to rule evaluators. */ export type DiscoveryDatasetMap = { - 'aws-apigateway-stages': AwsApiGatewayStage[]; 'aws-cloudtrail-trails': AwsCloudTrailTrail[]; 'aws-cloudfront-distributions': AwsCloudFrontDistribution[]; 'aws-cloudfront-distribution-request-activity': AwsCloudFrontDistributionRequestActivity[]; 'aws-cloudwatch-log-groups': AwsCloudWatchLogGroup[]; 'aws-cloudwatch-log-group-recent-stream-activity': AwsCloudWatchLogGroupRecentStreamActivity[]; - 'aws-cloudwatch-log-metric-filter-coverage': AwsCloudWatchLogMetricFilterCoverage[]; 'aws-cloudwatch-log-streams': AwsCloudWatchLogStream[]; 'aws-cost-usage': AwsCostUsage[]; 'aws-cost-anomaly-monitors': AwsCostAnomalyMonitor[]; @@ -826,14 +802,7 @@ export type DiscoveryDatasetMap = { }; /** Rule-facing static IaC dataset key exposed through the evaluation context. */ -export type StaticDatasetKey = SharedDatasetKey | 'aws-ec2-vpc-endpoints' | 'aws-lambda-provisioned-concurrency'; - -/** Normalized static API Gateway stage dataset entry. */ -export type AwsStaticApiGatewayStage = { - resourceId: string; - cacheClusterEnabled: boolean | null; - location?: SourceLocation; -}; +export type StaticDatasetKey = SharedDatasetKey | 'aws-ec2-vpc-endpoints'; /** Normalized static CloudFront distribution dataset entry. */ export type AwsStaticCloudFrontDistribution = { @@ -952,13 +921,6 @@ export type AwsStaticLambdaFunction = { location?: SourceLocation; }; -/** Normalized static Lambda provisioned concurrency dataset entry. */ -export type AwsStaticLambdaProvisionedConcurrency = { - resourceId: string; - provisionedConcurrentExecutions: number | null; - location?: SourceLocation; -}; - /** Normalized static ECS service dataset entry. */ export type AwsStaticEcsService = { resourceId: string; @@ -1013,7 +975,6 @@ export type AwsStaticS3BucketAnalysis = AwsS3BucketAnalysisFlags & { /** Normalized static datasets available to rule evaluators. */ export type StaticDatasetMap = { - 'aws-apigateway-stages': AwsStaticApiGatewayStage[]; 'aws-cloudfront-distributions': AwsStaticCloudFrontDistribution[]; 'aws-cloudwatch-log-groups': AwsStaticCloudWatchLogGroup[]; 'aws-dynamodb-autoscaling': AwsStaticDynamoDbAutoscaling[]; @@ -1028,7 +989,6 @@ export type StaticDatasetMap = { 'aws-eks-nodegroups': AwsStaticEksNodegroup[]; 'aws-emr-clusters': AwsStaticEmrCluster[]; 'aws-lambda-functions': AwsStaticLambdaFunction[]; - 'aws-lambda-provisioned-concurrency': AwsStaticLambdaProvisionedConcurrency[]; 'aws-ec2-vpc-endpoints': AwsStaticEc2VpcEndpoint[]; 'aws-rds-instances': AwsStaticRdsInstance[]; 'aws-redshift-clusters': AwsStaticRedshiftCluster[]; diff --git a/packages/rules/test/apigateway-caching-disabled.test.ts b/packages/rules/test/apigateway-caching-disabled.test.ts deleted file mode 100644 index d10c42b..0000000 --- a/packages/rules/test/apigateway-caching-disabled.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { apiGatewayCachingDisabledRule } from '../src/aws/apigateway/caching-disabled.js'; -import type { AwsApiGatewayStage, AwsStaticApiGatewayStage } from '../src/index.js'; -import { LiveResourceBag, StaticResourceBag } from '../src/index.js'; - -const createStage = (overrides: Partial = {}): AwsApiGatewayStage => ({ - accountId: '123456789012', - cacheClusterEnabled: false, - region: 'us-east-1', - restApiId: 'a1b2c3d4', - stageArn: 'arn:aws:apigateway:us-east-1::/restapis/a1b2c3d4/stages/prod', - stageName: 'prod', - ...overrides, -}); - -const createStaticStage = (overrides: Partial = {}): AwsStaticApiGatewayStage => ({ - resourceId: 'aws_api_gateway_stage.prod', - cacheClusterEnabled: false, - ...overrides, -}); - -describe('apiGatewayCachingDisabledRule', () => { - it('flags REST API stages with caching disabled', () => { - const finding = apiGatewayCachingDisabledRule.evaluateLive?.({ - catalog: { - indexType: 'LOCAL', - resources: [], - searchRegion: 'us-east-1', - }, - resources: new LiveResourceBag({ - 'aws-apigateway-stages': [createStage()], - }), - }); - - expect(finding?.findings).toEqual([ - { - accountId: '123456789012', - region: 'us-east-1', - resourceId: 'arn:aws:apigateway:us-east-1::/restapis/a1b2c3d4/stages/prod', - }, - ]); - }); - - it('does not flag REST API stages with caching enabled', () => { - const finding = apiGatewayCachingDisabledRule.evaluateLive?.({ - catalog: { - indexType: 'LOCAL', - resources: [], - searchRegion: 'us-east-1', - }, - resources: new LiveResourceBag({ - 'aws-apigateway-stages': [createStage({ cacheClusterEnabled: true })], - }), - }); - - expect(finding).toBeNull(); - }); - - it('flags static REST API stages with caching disabled', () => { - const finding = apiGatewayCachingDisabledRule.evaluateStatic?.({ - resources: new StaticResourceBag({ - 'aws-apigateway-stages': [createStaticStage()], - }), - }); - - expect(finding?.findings).toEqual([ - { - resourceId: 'aws_api_gateway_stage.prod', - }, - ]); - }); - - it('does not flag static REST API stages with caching enabled or unknown state', () => { - const enabledFinding = apiGatewayCachingDisabledRule.evaluateStatic?.({ - resources: new StaticResourceBag({ - 'aws-apigateway-stages': [createStaticStage({ cacheClusterEnabled: true })], - }), - }); - const unknownFinding = apiGatewayCachingDisabledRule.evaluateStatic?.({ - resources: new StaticResourceBag({ - 'aws-apigateway-stages': [createStaticStage({ cacheClusterEnabled: null })], - }), - }); - - expect(enabledFinding).toBeNull(); - expect(unknownFinding).toBeNull(); - }); -}); diff --git a/packages/rules/test/cloudwatch-log-group-no-metric-filters.test.ts b/packages/rules/test/cloudwatch-log-group-no-metric-filters.test.ts deleted file mode 100644 index 88b0d6c..0000000 --- a/packages/rules/test/cloudwatch-log-group-no-metric-filters.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { cloudWatchLogGroupNoMetricFiltersRule } from '../src/aws/cloudwatch/log-group-no-metric-filters.js'; -import type { AwsCloudWatchLogGroup, AwsCloudWatchLogMetricFilterCoverage } from '../src/index.js'; -import { LiveResourceBag } from '../src/index.js'; - -const createLogGroup = (overrides: Partial = {}): AwsCloudWatchLogGroup => ({ - accountId: '123456789012', - logGroupArn: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/app', - logGroupName: '/aws/lambda/app', - region: 'us-east-1', - storedBytes: 2_147_483_648, - ...overrides, -}); - -const createMetricFilterCoverage = ( - overrides: Partial = {}, -): AwsCloudWatchLogMetricFilterCoverage => ({ - accountId: '123456789012', - logGroupName: '/aws/lambda/app', - metricFilterCount: 0, - region: 'us-east-1', - ...overrides, -}); - -describe('cloudWatchLogGroupNoMetricFiltersRule', () => { - it('flags 1 GB+ log groups with zero metric filters', () => { - const finding = cloudWatchLogGroupNoMetricFiltersRule.evaluateLive?.({ - catalog: { - indexType: 'LOCAL', - resources: [], - searchRegion: 'us-east-1', - }, - resources: new LiveResourceBag({ - 'aws-cloudwatch-log-groups': [createLogGroup()], - 'aws-cloudwatch-log-metric-filter-coverage': [createMetricFilterCoverage()], - }), - }); - - expect(finding).toEqual({ - ruleId: 'CLDBRN-AWS-CLOUDWATCH-3', - service: 'cloudwatch', - severity: 'low', - source: 'discovery', - message: - 'CloudWatch log groups storing at least 1 GB should define metric filters or reduce retention aggressively.', - findings: [ - { - accountId: '123456789012', - region: 'us-east-1', - resourceId: '/aws/lambda/app', - }, - ], - }); - }); - - it('skips smaller log groups', () => { - const finding = cloudWatchLogGroupNoMetricFiltersRule.evaluateLive?.({ - catalog: { - indexType: 'LOCAL', - resources: [], - searchRegion: 'us-east-1', - }, - resources: new LiveResourceBag({ - 'aws-cloudwatch-log-groups': [createLogGroup({ storedBytes: 1_073_741_823 })], - 'aws-cloudwatch-log-metric-filter-coverage': [createMetricFilterCoverage()], - }), - }); - - expect(finding).toBeNull(); - }); - - it('skips log groups with metric filters', () => { - const finding = cloudWatchLogGroupNoMetricFiltersRule.evaluateLive?.({ - catalog: { - indexType: 'LOCAL', - resources: [], - searchRegion: 'us-east-1', - }, - resources: new LiveResourceBag({ - 'aws-cloudwatch-log-groups': [createLogGroup()], - 'aws-cloudwatch-log-metric-filter-coverage': [createMetricFilterCoverage({ metricFilterCount: 2 })], - }), - }); - - expect(finding).toBeNull(); - }); -}); diff --git a/packages/rules/test/exports.test.ts b/packages/rules/test/exports.test.ts index 6a7acc8..baee16d 100644 --- a/packages/rules/test/exports.test.ts +++ b/packages/rules/test/exports.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest'; import type { - AwsApiGatewayStage, AwsCloudFrontDistribution, AwsCloudFrontDistributionRequestActivity, AwsCloudTrailTrail, @@ -53,22 +52,20 @@ import { StaticResourceBag, } from '../src/index.js'; +const awsRuleIds = awsRules.map((rule) => rule.id); + describe('rule exports', () => { it('exports non-empty AWS rules and preset IDs', () => { - expect(awsRules.length).toBeGreaterThan(0); - expect(awsCorePreset.ruleIds).toEqual( - awsRules.map((rule) => rule.id).filter((ruleId) => ruleId !== 'CLDBRN-AWS-TAGGING-1'), - ); - expect(awsRules.map((rule) => rule.id)).toEqual( + expect(awsRuleIds).toHaveLength(81); + expect(awsCorePreset.ruleIds).toEqual(awsRuleIds.filter((ruleId) => ruleId !== 'CLDBRN-AWS-TAGGING-1')); + expect(awsRuleIds).toEqual( expect.arrayContaining([ - 'CLDBRN-AWS-APIGATEWAY-1', 'CLDBRN-AWS-CLOUDFRONT-1', 'CLDBRN-AWS-CLOUDFRONT-2', 'CLDBRN-AWS-CLOUDTRAIL-1', 'CLDBRN-AWS-CLOUDTRAIL-2', 'CLDBRN-AWS-CLOUDWATCH-1', 'CLDBRN-AWS-CLOUDWATCH-2', - 'CLDBRN-AWS-CLOUDWATCH-3', 'CLDBRN-AWS-COSTGUARDRAILS-1', 'CLDBRN-AWS-COSTGUARDRAILS-2', 'CLDBRN-AWS-COSTGUARDRAILS-3', @@ -117,7 +114,6 @@ describe('rule exports', () => { 'CLDBRN-AWS-LAMBDA-2', 'CLDBRN-AWS-LAMBDA-3', 'CLDBRN-AWS-LAMBDA-4', - 'CLDBRN-AWS-LAMBDA-5', 'CLDBRN-AWS-RDS-2', 'CLDBRN-AWS-RDS-3', 'CLDBRN-AWS-RDS-4', @@ -150,14 +146,6 @@ describe('rule exports', () => { expect(LiveResourceBag).toBeTypeOf('function'); expect(StaticResourceBag).toBeTypeOf('function'); - const apiGatewayStage: AwsApiGatewayStage = { - accountId: '123456789012', - cacheClusterEnabled: false, - region: 'us-east-1', - restApiId: 'a1b2c3d4', - stageArn: 'arn:aws:apigateway:us-east-1::/restapis/a1b2c3d4/stages/prod', - stageName: 'prod', - }; const instance: AwsEc2Instance = { accountId: '123456789012', architecture: 'x86_64', @@ -174,9 +162,6 @@ describe('rule exports', () => { expect(instance.architecture).toBe('x86_64'); expect(instance.launchTime).toBe('2026-03-01T00:00:00.000Z'); expect(instance.stoppedAt).toBe('2026-03-15T00:00:00.000Z'); - expect(apiGatewayStage.stageName).toBe('prod'); - expect(apiGatewayStage.cacheClusterEnabled).toBe(false); - const volume: AwsEbsVolume = { accountId: '123456789012', iops: 12000, @@ -481,7 +466,6 @@ describe('rule exports', () => { totalInvocationsLast14Days: 0, }; - const apiGatewayDatasetKey: DiscoveryDatasetKey = 'aws-apigateway-stages'; const cloudFrontDatasetKey: DiscoveryDatasetKey = 'aws-cloudfront-distributions'; const cloudFrontRequestActivityDatasetKey: DiscoveryDatasetKey = 'aws-cloudfront-distribution-request-activity'; const datasetKey: DiscoveryDatasetKey = 'aws-rds-instances'; @@ -514,7 +498,6 @@ describe('rule exports', () => { const targetGroupDatasetKey: DiscoveryDatasetKey = 'aws-ec2-target-groups'; const staticDatasetKey: StaticDatasetKey = 'aws-rds-instances'; - expect(apiGatewayDatasetKey).toBe('aws-apigateway-stages'); expect(cloudFrontDatasetKey).toBe('aws-cloudfront-distributions'); expect(cloudFrontRequestActivityDatasetKey).toBe('aws-cloudfront-distribution-request-activity'); expect(datasetKey).toBe('aws-rds-instances'); diff --git a/packages/rules/test/lambda-provisioned-concurrency-configured.test.ts b/packages/rules/test/lambda-provisioned-concurrency-configured.test.ts deleted file mode 100644 index 8f0da48..0000000 --- a/packages/rules/test/lambda-provisioned-concurrency-configured.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { lambdaProvisionedConcurrencyConfiguredRule } from '../src/aws/lambda/provisioned-concurrency-configured.js'; -import type { AwsStaticLambdaProvisionedConcurrency } from '../src/index.js'; -import { StaticResourceBag } from '../src/index.js'; - -const createConfig = ( - overrides: Partial = {}, -): AwsStaticLambdaProvisionedConcurrency => ({ - location: { - path: 'main.tf', - line: 5, - column: 3, - }, - provisionedConcurrentExecutions: 10, - resourceId: 'aws_lambda_provisioned_concurrency_config.worker', - ...overrides, -}); - -describe('lambdaProvisionedConcurrencyConfiguredRule', () => { - it('flags explicit provisioned concurrency configuration', () => { - const finding = lambdaProvisionedConcurrencyConfiguredRule.evaluateStatic?.({ - resources: new StaticResourceBag({ - 'aws-lambda-provisioned-concurrency': [createConfig()], - }), - }); - - expect(finding).toEqual({ - ruleId: 'CLDBRN-AWS-LAMBDA-5', - service: 'lambda', - severity: 'high', - source: 'iac', - message: 'Lambda provisioned concurrency should be reviewed for steady low-latency demand.', - findings: [ - { - location: { - path: 'main.tf', - line: 5, - column: 3, - }, - resourceId: 'aws_lambda_provisioned_concurrency_config.worker', - }, - ], - }); - }); - - it('skips empty or zero-valued provisioned concurrency configuration', () => { - const finding = lambdaProvisionedConcurrencyConfiguredRule.evaluateStatic?.({ - resources: new StaticResourceBag({ - 'aws-lambda-provisioned-concurrency': [ - createConfig({ provisionedConcurrentExecutions: 0 }), - createConfig({ resourceId: 'WorkerAlias', provisionedConcurrentExecutions: null }), - ], - }), - }); - - expect(finding).toBeNull(); - }); -}); diff --git a/packages/rules/test/rule-metadata.test.ts b/packages/rules/test/rule-metadata.test.ts index c0a3f2b..c422ecf 100644 --- a/packages/rules/test/rule-metadata.test.ts +++ b/packages/rules/test/rule-metadata.test.ts @@ -141,23 +141,6 @@ describe('rule metadata', () => { }); }); - it('defines the expected CloudWatch no-metric-filters rule metadata', () => { - const rule = awsRules.find((candidate) => candidate.id === 'CLDBRN-AWS-CLOUDWATCH-3'); - - expect(rule).toBeDefined(); - expect(rule).toMatchObject({ - id: 'CLDBRN-AWS-CLOUDWATCH-3', - name: 'CloudWatch Log Group No Metric Filters', - description: 'Flag CloudWatch log groups storing at least 1 GB when they define no metric filters.', - message: - 'CloudWatch log groups storing at least 1 GB should define metric filters or reduce retention aggressively.', - provider: 'aws', - service: 'cloudwatch', - supports: ['discovery'], - discoveryDependencies: ['aws-cloudwatch-log-groups', 'aws-cloudwatch-log-metric-filter-coverage'], - }); - }); - it('defines the expected S3 lifecycle rule metadata', () => { const rule = awsRules.find((candidate) => candidate.id === 'CLDBRN-AWS-S3-1'); @@ -909,22 +892,6 @@ describe('rule metadata', () => { }); }); - it('defines the expected Lambda provisioned-concurrency rule metadata', () => { - const rule = awsRules.find((candidate) => candidate.id === 'CLDBRN-AWS-LAMBDA-5'); - - expect(rule).toBeDefined(); - expect(rule).toMatchObject({ - id: 'CLDBRN-AWS-LAMBDA-5', - name: 'Lambda Provisioned Concurrency Configured', - description: 'Flag explicit Lambda provisioned concurrency configuration for cost review.', - message: 'Lambda provisioned concurrency should be reviewed for steady low-latency demand.', - provider: 'aws', - service: 'lambda', - supports: ['iac'], - staticDependencies: ['aws-lambda-provisioned-concurrency'], - }); - }); - it('defines the expected RDS idle-instance rule metadata', () => { const rule = awsRules.find((candidate) => candidate.id === 'CLDBRN-AWS-RDS-2'); @@ -1122,23 +1089,6 @@ describe('rule metadata', () => { }); }); - it('defines the expected API Gateway caching-disabled rule metadata', () => { - const rule = awsRules.find((candidate) => candidate.id === 'CLDBRN-AWS-APIGATEWAY-1'); - - expect(rule).toBeDefined(); - expect(rule).toMatchObject({ - id: 'CLDBRN-AWS-APIGATEWAY-1', - name: 'API Gateway Stage Caching Disabled', - description: 'Flag API Gateway REST API stages with caching disabled.', - message: 'API Gateway REST API stages should enable caching when stage caching is available.', - provider: 'aws', - service: 'apigateway', - supports: ['discovery', 'iac'], - discoveryDependencies: ['aws-apigateway-stages'], - staticDependencies: ['aws-apigateway-stages'], - }); - }); - it('defines the expected SageMaker notebook-running rule metadata', () => { const rule = awsRules.find((candidate) => candidate.id === 'CLDBRN-AWS-SAGEMAKER-1'); diff --git a/packages/rules/test/s3-intelligent-tiering-recommendation.test.ts b/packages/rules/test/s3-intelligent-tiering-recommendation.test.ts deleted file mode 100644 index 8395891..0000000 --- a/packages/rules/test/s3-intelligent-tiering-recommendation.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { s3IntelligentTieringRecommendationRule } from '../src/aws/s3/intelligent-tiering-recommendation.js'; -import type { AwsS3BucketAnalysis, AwsStaticS3BucketAnalysis } from '../src/index.js'; -import { LiveResourceBag, StaticResourceBag } from '../src/index.js'; - -const RULE_MESSAGE = - 'S3 buckets without any storage-class transition should enable Intelligent-Tiering when access patterns are unknown.'; - -const createBucketAnalysis = (overrides: Partial = {}): AwsStaticS3BucketAnalysis => ({ - hasAbortIncompleteMultipartUploadAfter7Days: false, - hasAlternativeStorageClassTransition: false, - hasCostFocusedLifecycle: false, - hasIntelligentTieringConfiguration: false, - hasIntelligentTieringTransition: false, - hasLifecycleSignal: false, - hasNoncurrentVersionCleanup: false, - hasUnclassifiedTransition: false, - location: { - path: 'main.tf', - line: 1, - column: 1, - }, - resourceId: 'aws_s3_bucket.logs', - versioningEnabled: false, - ...overrides, -}); - -const createLiveBucketAnalysis = (overrides: Partial = {}): AwsS3BucketAnalysis => ({ - accountId: '123456789012', - bucketName: 'logs-bucket', - hasAbortIncompleteMultipartUploadAfter7Days: false, - hasAlternativeStorageClassTransition: false, - hasCostFocusedLifecycle: false, - hasIntelligentTieringConfiguration: false, - hasIntelligentTieringTransition: false, - hasLifecycleSignal: false, - hasUnclassifiedTransition: false, - region: 'us-east-1', - ...overrides, -}); - -describe('s3IntelligentTieringRecommendationRule', () => { - it('flags live buckets with no lifecycle configuration and no Intelligent-Tiering configuration', () => { - const finding = s3IntelligentTieringRecommendationRule.evaluateLive?.({ - catalog: { - resources: [], - searchRegion: 'us-east-1', - indexType: 'LOCAL', - }, - resources: new LiveResourceBag({ - 'aws-s3-bucket-analyses': [createLiveBucketAnalysis()], - }), - }); - - expect(s3IntelligentTieringRecommendationRule.discoveryDependencies).toEqual(['aws-s3-bucket-analyses']); - expect(s3IntelligentTieringRecommendationRule.staticDependencies).toEqual(['aws-s3-bucket-analyses']); - expect(finding).toEqual({ - ruleId: 'CLDBRN-AWS-S3-5', - service: 's3', - severity: 'low', - source: 'discovery', - message: RULE_MESSAGE, - findings: [ - { - resourceId: 'logs-bucket', - region: 'us-east-1', - accountId: '123456789012', - }, - ], - }); - }); - - it('passes live buckets that already have an Intelligent-Tiering configuration', () => { - const finding = s3IntelligentTieringRecommendationRule.evaluateLive?.({ - catalog: { - resources: [], - searchRegion: 'us-east-1', - indexType: 'LOCAL', - }, - resources: new LiveResourceBag({ - 'aws-s3-bucket-analyses': [createLiveBucketAnalysis({ hasIntelligentTieringConfiguration: true })], - }), - }); - - expect(finding).toBeNull(); - }); - - it('passes live buckets with lifecycle rules so CLDBRN-AWS-S3-2 keeps ownership of them', () => { - const finding = s3IntelligentTieringRecommendationRule.evaluateLive?.({ - catalog: { - resources: [], - searchRegion: 'us-east-1', - indexType: 'LOCAL', - }, - resources: new LiveResourceBag({ - 'aws-s3-bucket-analyses': [ - createLiveBucketAnalysis({ - hasCostFocusedLifecycle: true, - hasLifecycleSignal: true, - }), - ], - }), - }); - - expect(finding).toBeNull(); - }); - - it('flags Terraform buckets with no lifecycle configuration and no Intelligent-Tiering configuration', () => { - const finding = s3IntelligentTieringRecommendationRule.evaluateStatic?.({ - resources: new StaticResourceBag({ - 'aws-s3-bucket-analyses': [createBucketAnalysis()], - }), - }); - - expect(finding).toEqual({ - ruleId: 'CLDBRN-AWS-S3-5', - service: 's3', - severity: 'low', - source: 'iac', - message: RULE_MESSAGE, - findings: [ - { - resourceId: 'aws_s3_bucket.logs', - location: { - path: 'main.tf', - line: 1, - column: 1, - }, - }, - ], - }); - }); - - it('flags CloudFormation buckets with no lifecycle configuration and no Intelligent-Tiering configuration', () => { - const finding = s3IntelligentTieringRecommendationRule.evaluateStatic?.({ - resources: new StaticResourceBag({ - 'aws-s3-bucket-analyses': [ - createBucketAnalysis({ - location: { - path: 'template.yaml', - line: 3, - column: 3, - }, - resourceId: 'LogsBucket', - }), - ], - }), - }); - - expect(finding).toEqual({ - ruleId: 'CLDBRN-AWS-S3-5', - service: 's3', - severity: 'low', - source: 'iac', - message: RULE_MESSAGE, - findings: [ - { - resourceId: 'LogsBucket', - location: { - path: 'template.yaml', - line: 3, - column: 3, - }, - }, - ], - }); - }); - - it('passes Terraform buckets that already declare an Intelligent-Tiering configuration', () => { - const finding = s3IntelligentTieringRecommendationRule.evaluateStatic?.({ - resources: new StaticResourceBag({ - 'aws-s3-bucket-analyses': [createBucketAnalysis({ hasIntelligentTieringConfiguration: true })], - }), - }); - - expect(finding).toBeNull(); - }); - - it('returns null when there are no buckets to evaluate', () => { - const finding = s3IntelligentTieringRecommendationRule.evaluateStatic?.({ - resources: new StaticResourceBag({ - 'aws-s3-bucket-analyses': [], - }), - }); - - expect(finding).toBeNull(); - }); -}); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index a65d830..f739591 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -43,7 +43,6 @@ "dev": "tsup --watch" }, "dependencies": { - "@aws-sdk/client-api-gateway": "^3.1015.0", "@aws-sdk/client-application-auto-scaling": "^3.1009.0", "@aws-sdk/client-budgets": "^3.1015.0", "@aws-sdk/client-cloudfront": "^3.1015.0", diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index e45ccab..98c3a78 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -13,7 +13,6 @@ export { export { isAwsDiscoveryErrorCode } from './providers/aws/errors.js'; export { CloudBurnClient } from './scanner.js'; export type { - AwsApiGatewayStage, AwsCloudFrontDistribution, AwsCloudTrailTrail, AwsCloudWatchLogGroup, diff --git a/packages/sdk/src/providers/aws/client.ts b/packages/sdk/src/providers/aws/client.ts index 652a2bb..3145455 100644 --- a/packages/sdk/src/providers/aws/client.ts +++ b/packages/sdk/src/providers/aws/client.ts @@ -1,5 +1,4 @@ import { AsyncLocalStorage } from 'node:async_hooks'; -import { APIGatewayClient } from '@aws-sdk/client-api-gateway'; import { ApplicationAutoScalingClient } from '@aws-sdk/client-application-auto-scaling'; import { BudgetsClient } from '@aws-sdk/client-budgets'; import { CloudFrontClient } from '@aws-sdk/client-cloudfront'; @@ -188,14 +187,6 @@ export const createApplicationAutoScalingClient = (config: AwsClientConfig): App credentials: resolveAwsClientCredentials(), }); -/** Creates an AWS API Gateway REST API client for a specific region. */ -export const createApiGatewayClient = (config: AwsClientConfig): APIGatewayClient => - new APIGatewayClient({ - ...baseAwsClientConfig(), - region: config.region, - credentials: resolveAwsClientCredentials(), - }); - /** Creates an AWS Budgets client against the global billing control plane. */ export const createBudgetsClient = (): BudgetsClient => new BudgetsClient({ diff --git a/packages/sdk/src/providers/aws/discovery-registry.ts b/packages/sdk/src/providers/aws/discovery-registry.ts index 5e9b124..a8442d2 100644 --- a/packages/sdk/src/providers/aws/discovery-registry.ts +++ b/packages/sdk/src/providers/aws/discovery-registry.ts @@ -8,7 +8,6 @@ import { type Rule, } from '@cloudburn/rules'; import type { EvaluatedResource, ScanDiagnostic } from '../../types.js'; -import { hydrateAwsApiGatewayStages } from './resources/apigateway.js'; import { hydrateAwsCloudFrontDistributionRequestActivity, hydrateAwsCloudFrontDistributions, @@ -17,7 +16,6 @@ import { hydrateAwsCloudTrailTrails } from './resources/cloudtrail.js'; import { hydrateAwsCloudWatchLogGroupRecentStreamActivity, hydrateAwsCloudWatchLogGroups, - hydrateAwsCloudWatchLogMetricFilterCoverage, hydrateAwsCloudWatchLogStreams, } from './resources/cloudwatch-logs.js'; import { hydrateAwsCostUsage } from './resources/cost-explorer.js'; @@ -106,7 +104,6 @@ export type AwsDiscoveryDatasetDefinition EvaluationResourceProjection[]; resourceTypes: string[]; service: - | 'apigateway' | 'cloudfront' | 'cloudtrail' | 'cloudwatch' @@ -197,13 +194,6 @@ const awsRuleEvaluationOverrides: Record = { const awsDiscoveryDatasetRegistry: { [K in DiscoveryDatasetKey]: AwsDiscoveryDatasetDefinition; } = { - 'aws-apigateway-stages': { - datasetKey: 'aws-apigateway-stages', - resourceTypes: ['apigateway:restapis/stages'], - service: 'apigateway', - load: hydrateAwsApiGatewayStages, - toEvaluationResources: (stages) => mapEvaluationResources(stages, (stage) => stage.stageArn), - }, 'aws-cloudtrail-trails': { datasetKey: 'aws-cloudtrail-trails', resourceTypes: ['cloudtrail:trail'], @@ -260,12 +250,6 @@ const awsDiscoveryDatasetRegistry: { }), ), }, - 'aws-cloudwatch-log-metric-filter-coverage': { - datasetKey: 'aws-cloudwatch-log-metric-filter-coverage', - resourceTypes: ['logs:log-group'], - service: 'cloudwatch', - load: hydrateAwsCloudWatchLogMetricFilterCoverage, - }, 'aws-cloudwatch-log-streams': { datasetKey: 'aws-cloudwatch-log-streams', resourceTypes: ['logs:log-group'], diff --git a/packages/sdk/src/providers/aws/index.ts b/packages/sdk/src/providers/aws/index.ts index 8631358..3667f9b 100644 --- a/packages/sdk/src/providers/aws/index.ts +++ b/packages/sdk/src/providers/aws/index.ts @@ -1,5 +1,4 @@ export { - createApiGatewayClient, createCloudFrontClient, createCloudTrailClient, createCloudWatchClient, diff --git a/packages/sdk/src/providers/aws/resources/apigateway.ts b/packages/sdk/src/providers/aws/resources/apigateway.ts deleted file mode 100644 index 962a404..0000000 --- a/packages/sdk/src/providers/aws/resources/apigateway.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { GetStageCommand } from '@aws-sdk/client-api-gateway'; -import type { AwsApiGatewayStage, AwsDiscoveredResource } from '@cloudburn/rules'; -import { createApiGatewayClient } from '../client.js'; -import { chunkItems, withAwsServiceErrorContext } from './utils.js'; - -const API_GATEWAY_STAGE_CONCURRENCY = 10; - -type ParsedApiGatewayStage = { - restApiId: string; - stageArn: string; - stageName: string; -}; - -const parseApiGatewayStageArn = (arn: string): ParsedApiGatewayStage | null => { - const match = /^arn:[^:]+:apigateway:([^:]+)::\/restapis\/([^/]+)\/stages\/([^/]+)$/u.exec(arn); - - if (!match) { - return null; - } - - const region = match[1]; - const restApiId = match[2]; - const stageName = match[3]; - - if (!region || !restApiId || !stageName) { - return null; - } - - return { - restApiId, - stageArn: arn, - stageName, - }; -}; - -/** - * Hydrates discovered API Gateway REST API stages with stage-cache metadata. - * - * @param resources - Catalog resources filtered to API Gateway REST API stages. - * @returns Hydrated API Gateway stages for rule evaluation. - */ -export const hydrateAwsApiGatewayStages = async (resources: AwsDiscoveredResource[]): Promise => { - const stagesByRegion = new Map>(); - - for (const resource of resources) { - const parsed = parseApiGatewayStageArn(resource.arn); - - if (!parsed) { - continue; - } - - const regionStages = stagesByRegion.get(resource.region) ?? []; - regionStages.push({ - accountId: resource.accountId, - ...parsed, - }); - stagesByRegion.set(resource.region, regionStages); - } - - const hydratedPages = await Promise.all( - [...stagesByRegion.entries()].map(async ([region, regionStages]) => { - const client = createApiGatewayClient({ region }); - const stages: AwsApiGatewayStage[] = []; - - for (const batch of chunkItems(regionStages, API_GATEWAY_STAGE_CONCURRENCY)) { - const hydratedBatch = await Promise.all( - batch.map(async (stage) => { - const response = await withAwsServiceErrorContext('Amazon API Gateway', 'GetStage', region, () => - client.send( - new GetStageCommand({ - restApiId: stage.restApiId, - stageName: stage.stageName, - }), - ), - ); - - return { - accountId: stage.accountId, - cacheClusterEnabled: response.cacheClusterEnabled, - region, - restApiId: stage.restApiId, - stageArn: stage.stageArn, - stageName: response.stageName ?? stage.stageName, - } satisfies AwsApiGatewayStage; - }), - ); - - stages.push(...hydratedBatch); - } - - return stages; - }), - ); - - return hydratedPages.flat().sort((left, right) => left.stageArn.localeCompare(right.stageArn)); -}; diff --git a/packages/sdk/src/providers/aws/resources/cloudwatch-logs.ts b/packages/sdk/src/providers/aws/resources/cloudwatch-logs.ts index 452873a..83c899b 100644 --- a/packages/sdk/src/providers/aws/resources/cloudwatch-logs.ts +++ b/packages/sdk/src/providers/aws/resources/cloudwatch-logs.ts @@ -1,12 +1,7 @@ -import { - DescribeLogGroupsCommand, - DescribeLogStreamsCommand, - DescribeMetricFiltersCommand, -} from '@aws-sdk/client-cloudwatch-logs'; +import { DescribeLogGroupsCommand, DescribeLogStreamsCommand } from '@aws-sdk/client-cloudwatch-logs'; import type { AwsCloudWatchLogGroup, AwsCloudWatchLogGroupRecentStreamActivity, - AwsCloudWatchLogMetricFilterCoverage, AwsCloudWatchLogStream, AwsDiscoveredResource, } from '@cloudburn/rules'; @@ -279,82 +274,3 @@ export const hydrateAwsCloudWatchLogGroupRecentStreamActivity = async ( return hydratedPages.flat().sort((left, right) => left.logGroupName.localeCompare(right.logGroupName)); }; - -/** - * Hydrates discovered CloudWatch log groups with their metric-filter counts. - * - * @param resources - Catalog resources filtered to CloudWatch Logs log groups. - * @returns Metric-filter coverage summaries keyed by log group. - */ -export const hydrateAwsCloudWatchLogMetricFilterCoverage = async ( - resources: AwsDiscoveredResource[], -): Promise => { - const resourcesByRegion = new Map(); - - for (const resource of resources) { - const logGroupName = extractLogGroupName(resource.arn); - - if (!logGroupName) { - continue; - } - - const regionResources = resourcesByRegion.get(resource.region) ?? []; - regionResources.push(resource); - resourcesByRegion.set(resource.region, regionResources); - } - - const hydratedPages = await Promise.all( - [...resourcesByRegion.entries()].map(async ([region, regionResources]) => { - const client = createCloudWatchLogsClient({ region }); - const desiredLogGroups = new Map( - regionResources.flatMap((resource) => { - const logGroupName = extractLogGroupName(resource.arn); - - return logGroupName ? [[logGroupName, resource.accountId] as const] : []; - }), - ); - - const coverage: AwsCloudWatchLogMetricFilterCoverage[] = []; - - for (const batch of chunkItems([...desiredLogGroups.entries()], CLOUDWATCH_LOG_GROUP_HYDRATION_CONCURRENCY)) { - const hydratedBatch = await Promise.all( - batch.map(async ([logGroupName, accountId]) => { - let nextToken: string | undefined; - let metricFilterCount = 0; - - do { - const response = await withAwsServiceErrorContext( - 'Amazon CloudWatch Logs', - 'DescribeMetricFilters', - region, - () => - client.send( - new DescribeMetricFiltersCommand({ - logGroupName, - nextToken, - }), - ), - ); - - metricFilterCount += (response.metricFilters ?? []).length; - nextToken = response.nextToken; - } while (nextToken); - - return { - accountId, - logGroupName, - metricFilterCount, - region, - } satisfies AwsCloudWatchLogMetricFilterCoverage; - }), - ); - - coverage.push(...hydratedBatch); - } - - return coverage; - }), - ); - - return hydratedPages.flat().sort((left, right) => left.logGroupName.localeCompare(right.logGroupName)); -}; diff --git a/packages/sdk/src/providers/aws/static-registry.ts b/packages/sdk/src/providers/aws/static-registry.ts index 5459d08..359df06 100644 --- a/packages/sdk/src/providers/aws/static-registry.ts +++ b/packages/sdk/src/providers/aws/static-registry.ts @@ -1,5 +1,4 @@ import type { - AwsStaticApiGatewayStage, AwsStaticCloudFrontDistribution, AwsStaticCloudWatchLogGroup, AwsStaticDynamoDbAutoscaling, @@ -15,7 +14,6 @@ import type { AwsStaticElastiCacheCluster, AwsStaticEmrCluster, AwsStaticLambdaFunction, - AwsStaticLambdaProvisionedConcurrency, AwsStaticRdsInstance, AwsStaticRedshiftCluster, AwsStaticRoute53HealthCheck, @@ -39,8 +37,6 @@ type AwsStaticDatasetDefinition = const TERRAFORM_EBS_VOLUME_TYPE = 'aws_ebs_volume'; const CLOUDFORMATION_EBS_VOLUME_TYPE = 'AWS::EC2::Volume'; -const TERRAFORM_API_GATEWAY_STAGE_TYPE = 'aws_api_gateway_stage'; -const CLOUDFORMATION_API_GATEWAY_STAGE_TYPE = 'AWS::ApiGateway::Stage'; const TERRAFORM_CLOUDFRONT_DISTRIBUTION_TYPE = 'aws_cloudfront_distribution'; const CLOUDFORMATION_CLOUDFRONT_DISTRIBUTION_TYPE = 'AWS::CloudFront::Distribution'; const TERRAFORM_CLOUDWATCH_LOG_GROUP_TYPE = 'aws_cloudwatch_log_group'; @@ -78,8 +74,6 @@ const CLOUDFORMATION_ROUTE53_RECORD_SET_TYPE = 'AWS::Route53::RecordSet'; const CLOUDFORMATION_ROUTE53_RECORD_SET_GROUP_TYPE = 'AWS::Route53::RecordSetGroup'; const CLOUDFORMATION_ROUTE53_HEALTH_CHECK_TYPE = 'AWS::Route53::HealthCheck'; const TERRAFORM_LAMBDA_TYPE = 'aws_lambda_function'; -const TERRAFORM_LAMBDA_PROVISIONED_CONCURRENCY_TYPE = 'aws_lambda_provisioned_concurrency_config'; -const CLOUDFORMATION_LAMBDA_ALIAS_TYPE = 'AWS::Lambda::Alias'; const CLOUDFORMATION_LAMBDA_TYPE = 'AWS::Lambda::Function'; const TERRAFORM_REDSHIFT_CLUSTER_TYPE = 'aws_redshift_cluster'; const TERRAFORM_REDSHIFT_SCHEDULED_ACTION_TYPE = 'aws_redshift_scheduled_action'; @@ -729,22 +723,6 @@ const getCloudFormationEmrInstanceTypes = (resource: IaCResource): string[] => { return instanceTypes; }; -const loadStaticApiGatewayStages = (resources: IaCResource[]): AwsStaticApiGatewayStage[] => - resources.map((resource) => { - const rawValue = - resource.type === TERRAFORM_API_GATEWAY_STAGE_TYPE - ? resource.attributes.cache_cluster_enabled - : isRecord(resource.attributes.Properties) - ? resource.attributes.Properties.CacheClusterEnabled - : undefined; - - return { - cacheClusterEnabled: rawValue === undefined ? false : getLiteralBoolean(rawValue), - location: pickLocation(resource, ['cache_cluster_enabled', 'Properties.CacheClusterEnabled']), - resourceId: toStaticResourceId(resource), - }; - }); - const loadStaticCloudFrontDistributions = (resources: IaCResource[]): AwsStaticCloudFrontDistribution[] => resources.map((resource) => { const rawValue = @@ -1187,40 +1165,6 @@ const loadStaticLambdaFunctions = (resources: IaCResource[]): AwsStaticLambdaFun location: pickLocation(resource, ['architectures', 'Properties.Architectures']), })); -const loadStaticLambdaProvisionedConcurrency = (resources: IaCResource[]): AwsStaticLambdaProvisionedConcurrency[] => - resources.flatMap((resource) => { - if (resource.type === TERRAFORM_LAMBDA_PROVISIONED_CONCURRENCY_TYPE) { - return [ - { - location: pickLocation(resource, ['provisioned_concurrent_executions']), - provisionedConcurrentExecutions: getLiteralNumberish(resource.attributes.provisioned_concurrent_executions), - resourceId: toStaticResourceId(resource), - }, - ]; - } - - if (resource.type === CLOUDFORMATION_LAMBDA_ALIAS_TYPE) { - const properties = isRecord(resource.attributes.Properties) ? resource.attributes.Properties : undefined; - const concurrencyConfig = isRecord(properties?.ProvisionedConcurrencyConfig) - ? properties.ProvisionedConcurrencyConfig - : undefined; - - if (!concurrencyConfig) { - return []; - } - - return [ - { - location: pickLocation(resource, ['Properties.ProvisionedConcurrencyConfig.ProvisionedConcurrentExecutions']), - provisionedConcurrentExecutions: getLiteralNumberish(concurrencyConfig.ProvisionedConcurrentExecutions), - resourceId: toStaticResourceId(resource), - }, - ]; - } - - return []; - }); - const loadStaticEcsServices = (resources: IaCResource[]): AwsStaticEcsService[] => resources.flatMap((resource) => { if (resource.type === TERRAFORM_ECS_SERVICE_TYPE) { @@ -1501,12 +1445,6 @@ const loadStaticS3BucketAnalyses = (resources: IaCResource[]): AwsStaticS3Bucket }; const awsStaticDatasetRegistry: Record = { - 'aws-apigateway-stages': { - datasetKey: 'aws-apigateway-stages', - sourceKinds: ['terraform', 'cloudformation'], - resourceTypes: [TERRAFORM_API_GATEWAY_STAGE_TYPE, CLOUDFORMATION_API_GATEWAY_STAGE_TYPE], - load: loadStaticApiGatewayStages, - }, 'aws-cloudfront-distributions': { datasetKey: 'aws-cloudfront-distributions', sourceKinds: ['terraform', 'cloudformation'], @@ -1610,12 +1548,6 @@ const awsStaticDatasetRegistry: Record { }); it('exports live dataset types from the package root', () => { - const apiGatewayStage: AwsApiGatewayStage = { - accountId: '123456789012', - cacheClusterEnabled: false, - region: 'us-east-1', - restApiId: 'a1b2c3d4', - stageArn: 'arn:aws:apigateway:us-east-1::/restapis/a1b2c3d4/stages/prod', - stageName: 'prod', - }; const trail: AwsCloudTrailTrail = { accountId: '123456789012', homeRegion: 'us-east-1', @@ -292,7 +283,6 @@ describe('sdk exports', () => { totalInvocationsLast14Days: 0, }; - expect(apiGatewayStage.stageName).toBe('prod'); expect(trail.trailName).toBe('org-trail'); expect(logGroup.retentionInDays).toBe(30); expect(logStream.logStreamName).toContain('[$LATEST]'); diff --git a/packages/sdk/test/providers/aws-apigateway-resource.test.ts b/packages/sdk/test/providers/aws-apigateway-resource.test.ts deleted file mode 100644 index e338205..0000000 --- a/packages/sdk/test/providers/aws-apigateway-resource.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { GetStageCommand } from '@aws-sdk/client-api-gateway'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { createApiGatewayClient } from '../../src/providers/aws/client.js'; -import { hydrateAwsApiGatewayStages } from '../../src/providers/aws/resources/apigateway.js'; - -vi.mock('../../src/providers/aws/client.js', () => ({ - createApiGatewayClient: vi.fn(), -})); - -const mockedCreateApiGatewayClient = vi.mocked(createApiGatewayClient); - -describe('hydrateAwsApiGatewayStages', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - it('hydrates discovered API Gateway stages with cache-cluster metadata', async () => { - mockedCreateApiGatewayClient.mockReturnValue({ - send: vi.fn(async (command: GetStageCommand) => { - expect(command.input).toEqual({ - restApiId: 'a1b2c3', - stageName: 'prod', - }); - - return { - cacheClusterEnabled: false, - stageName: 'prod', - }; - }), - } as never); - - await expect( - hydrateAwsApiGatewayStages([ - { - accountId: '123456789012', - arn: 'arn:aws:apigateway:us-east-1::/restapis/a1b2c3/stages/prod', - properties: [], - region: 'us-east-1', - resourceType: 'apigateway:restapis/stages', - service: 'apigateway', - }, - ]), - ).resolves.toEqual([ - { - accountId: '123456789012', - cacheClusterEnabled: false, - region: 'us-east-1', - restApiId: 'a1b2c3', - stageArn: 'arn:aws:apigateway:us-east-1::/restapis/a1b2c3/stages/prod', - stageName: 'prod', - }, - ]); - }); -}); diff --git a/packages/sdk/test/providers/aws-cloudwatch-logs-resource.test.ts b/packages/sdk/test/providers/aws-cloudwatch-logs-resource.test.ts index 55787de..c460964 100644 --- a/packages/sdk/test/providers/aws-cloudwatch-logs-resource.test.ts +++ b/packages/sdk/test/providers/aws-cloudwatch-logs-resource.test.ts @@ -1,14 +1,9 @@ -import type { - DescribeLogGroupsCommand, - DescribeLogStreamsCommand, - DescribeMetricFiltersCommand, -} from '@aws-sdk/client-cloudwatch-logs'; +import type { DescribeLogGroupsCommand, DescribeLogStreamsCommand } from '@aws-sdk/client-cloudwatch-logs'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createCloudWatchLogsClient } from '../../src/providers/aws/client.js'; import { hydrateAwsCloudWatchLogGroupRecentStreamActivity, hydrateAwsCloudWatchLogGroups, - hydrateAwsCloudWatchLogMetricFilterCoverage, hydrateAwsCloudWatchLogStreams, } from '../../src/providers/aws/resources/cloudwatch-logs.js'; @@ -432,116 +427,3 @@ describe('hydrateAwsCloudWatchLogGroupRecentStreamActivity', () => { ]); }); }); - -describe('hydrateAwsCloudWatchLogMetricFilterCoverage', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - it('hydrates discovered log groups with metric filter counts', async () => { - mockedCreateCloudWatchLogsClient.mockReturnValue({ - send: vi.fn(async (command: DescribeMetricFiltersCommand) => { - const input = command.input as { filterNamePrefix?: string; logGroupName?: string; nextToken?: string }; - - expect(input.filterNamePrefix).toBeUndefined(); - - if (input.nextToken === undefined) { - return { - metricFilters: [{ filterName: 'errors' }], - nextToken: 'page-2', - }; - } - - return { - metricFilters: [{ filterName: 'warnings' }], - }; - }), - } as never); - - await expect( - hydrateAwsCloudWatchLogMetricFilterCoverage([ - { - accountId: '123456789012', - arn: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/app', - properties: [], - region: 'us-east-1', - resourceType: 'logs:log-group', - service: 'logs', - }, - ]), - ).resolves.toEqual([ - { - accountId: '123456789012', - logGroupName: '/aws/lambda/app', - metricFilterCount: 2, - region: 'us-east-1', - }, - ]); - }); - - it('bounds concurrent DescribeMetricFilters calls within a region', async () => { - let inFlight = 0; - let maxInFlight = 0; - const send = vi.fn(async (_command: DescribeMetricFiltersCommand) => { - inFlight += 1; - maxInFlight = Math.max(maxInFlight, inFlight); - await new Promise((resolve) => { - setTimeout(resolve, 1); - }); - inFlight -= 1; - - return { metricFilters: [{ filterName: 'errors' }] }; - }); - - mockedCreateCloudWatchLogsClient.mockReturnValue({ send } as never); - - const coverage = await hydrateAwsCloudWatchLogMetricFilterCoverage( - Array.from({ length: 25 }, (_, index) => ({ - accountId: '123456789012', - arn: `arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/app-${index}`, - properties: [], - region: 'us-east-1', - resourceType: 'logs:log-group', - service: 'logs', - })), - ); - - expect(coverage).toHaveLength(25); - expect(send).toHaveBeenCalledTimes(25); - expect(maxInFlight).toBeLessThanOrEqual(10); - }); - - it('preserves CloudWatch Logs error identity when metric-filter hydration is access denied', async () => { - mockedCreateCloudWatchLogsClient.mockReturnValue({ - send: vi.fn().mockRejectedValue( - Object.assign(new Error('User is not authorized to perform: logs:DescribeMetricFilters'), { - name: 'AccessDeniedException', - code: 'AccessDeniedException', - $metadata: { - httpStatusCode: 403, - requestId: 'request-metric-filters', - }, - }), - ), - } as never); - - const error = await hydrateAwsCloudWatchLogMetricFilterCoverage([ - { - accountId: '123456789012', - arn: 'arn:aws:logs:eu-central-1:123456789012:log-group:/aws/lambda/app', - properties: [], - region: 'eu-central-1', - resourceType: 'logs:log-group', - service: 'logs', - }, - ]).catch((err) => err); - - expect(error).toMatchObject({ - code: 'AccessDeniedException', - name: 'AccessDeniedException', - }); - expect((error as Error).message).toBe( - 'Amazon CloudWatch Logs DescribeMetricFilters failed in eu-central-1 with AccessDeniedException: User is not authorized to perform: logs:DescribeMetricFilters Request ID: request-metric-filters.', - ); - }); -}); diff --git a/packages/sdk/test/providers/aws-discovery.test.ts b/packages/sdk/test/providers/aws-discovery.test.ts index c95ec8e..70b019e 100644 --- a/packages/sdk/test/providers/aws-discovery.test.ts +++ b/packages/sdk/test/providers/aws-discovery.test.ts @@ -21,7 +21,6 @@ import { waitForAwsResourceExplorerIndex, waitForAwsResourceExplorerSetup, } from '../../src/providers/aws/resource-explorer.js'; -import { hydrateAwsApiGatewayStages } from '../../src/providers/aws/resources/apigateway.js'; import { hydrateAwsCloudFrontDistributionRequestActivity, hydrateAwsCloudFrontDistributions, @@ -30,7 +29,6 @@ import { hydrateAwsCloudTrailTrails } from '../../src/providers/aws/resources/cl import { hydrateAwsCloudWatchLogGroupRecentStreamActivity, hydrateAwsCloudWatchLogGroups, - hydrateAwsCloudWatchLogMetricFilterCoverage, hydrateAwsCloudWatchLogStreams, } from '../../src/providers/aws/resources/cloudwatch-logs.js'; import { hydrateAwsCostUsage } from '../../src/providers/aws/resources/cost-explorer.js'; @@ -151,10 +149,6 @@ vi.mock('../../src/providers/aws/resources/cloudtrail.js', () => ({ hydrateAwsCloudTrailTrails: vi.fn(), })); -vi.mock('../../src/providers/aws/resources/apigateway.js', () => ({ - hydrateAwsApiGatewayStages: vi.fn(), -})); - vi.mock('../../src/providers/aws/resources/cloudfront.js', () => ({ hydrateAwsCloudFrontDistributionRequestActivity: vi.fn(), hydrateAwsCloudFrontDistributions: vi.fn(), @@ -163,7 +157,6 @@ vi.mock('../../src/providers/aws/resources/cloudfront.js', () => ({ vi.mock('../../src/providers/aws/resources/cloudwatch-logs.js', () => ({ hydrateAwsCloudWatchLogGroups: vi.fn(), hydrateAwsCloudWatchLogGroupRecentStreamActivity: vi.fn(), - hydrateAwsCloudWatchLogMetricFilterCoverage: vi.fn(), hydrateAwsCloudWatchLogStreams: vi.fn(), })); @@ -273,7 +266,6 @@ const mockedListAwsResourcesByFilter = vi.mocked(listAwsResourcesByFilter); const mockedUpdateAwsResourceExplorerIndexType = vi.mocked(updateAwsResourceExplorerIndexType); const mockedWaitForAwsResourceExplorerIndex = vi.mocked(waitForAwsResourceExplorerIndex); const mockedWaitForAwsResourceExplorerSetup = vi.mocked(waitForAwsResourceExplorerSetup); -const mockedHydrateAwsApiGatewayStages = vi.mocked(hydrateAwsApiGatewayStages); const mockedHydrateAwsCloudFrontDistributions = vi.mocked(hydrateAwsCloudFrontDistributions); const _mockedHydrateAwsCloudFrontDistributionRequestActivity = vi.mocked( hydrateAwsCloudFrontDistributionRequestActivity, @@ -283,7 +275,6 @@ const mockedHydrateAwsCloudWatchLogGroups = vi.mocked(hydrateAwsCloudWatchLogGro const mockedHydrateAwsCloudWatchLogGroupRecentStreamActivity = vi.mocked( hydrateAwsCloudWatchLogGroupRecentStreamActivity, ); -const mockedHydrateAwsCloudWatchLogMetricFilterCoverage = vi.mocked(hydrateAwsCloudWatchLogMetricFilterCoverage); const mockedHydrateAwsCloudWatchLogStreams = vi.mocked(hydrateAwsCloudWatchLogStreams); const mockedHydrateAwsCostUsage = vi.mocked(hydrateAwsCostUsage); const mockedHydrateAwsCostAnomalyMonitors = vi.mocked(hydrateAwsCostAnomalyMonitors); @@ -744,16 +735,6 @@ describe('discoverAwsResources', () => { }; mockedBuildAwsDiscoveryCatalog.mockResolvedValue(extendedCatalog); - mockedHydrateAwsApiGatewayStages.mockResolvedValue([ - { - accountId: '123456789012', - cacheClusterEnabled: false, - region: 'us-east-1', - restApiId: 'a1b2c3', - stageArn: 'arn:aws:apigateway:us-east-1::/restapis/a1b2c3/stages/prod', - stageName: 'prod', - }, - ]); mockedHydrateAwsCloudFrontDistributions.mockResolvedValue([ { accountId: '123456789012', @@ -845,10 +826,6 @@ describe('discoverAwsResources', () => { const result = await discoverAwsResources( [ - createRule({ - service: 'apigateway', - discoveryDependencies: ['aws-apigateway-stages'], - }), createRule({ id: 'CLDBRN-AWS-TEST-2', service: 'cloudfront', @@ -879,14 +856,12 @@ describe('discoverAwsResources', () => { ); expect(mockedBuildAwsDiscoveryCatalog).toHaveBeenCalledWith({ mode: 'regions', regions: ['us-east-1'] }, [ - 'apigateway:restapis/stages', 'cloudfront:distribution', 'dynamodb:table', 'route53:healthcheck', 'route53:hostedzone', 'secretsmanager:secret', ]); - expect(mockedHydrateAwsApiGatewayStages).toHaveBeenCalledWith([extendedCatalog.resources[0]], loadContextMatcher); expect(mockedHydrateAwsCloudFrontDistributions).toHaveBeenCalledWith( [extendedCatalog.resources[1]], loadContextMatcher, @@ -1919,48 +1894,6 @@ describe('discoverAwsResources', () => { ]); }); - it('hydrates CloudWatch log metric-filter coverage from log-group catalog resources', async () => { - mockedBuildAwsDiscoveryCatalog.mockResolvedValue({ - indexType: 'LOCAL', - resources: [catalog.resources[7]], - searchRegion: 'us-east-1', - }); - mockedHydrateAwsCloudWatchLogMetricFilterCoverage.mockResolvedValue([ - { - accountId: '123456789012', - logGroupName: '/aws/lambda/app', - metricFilterCount: 0, - region: 'us-east-1', - }, - ]); - - const result = await discoverAwsResources( - [ - createRule({ - discoveryDependencies: ['aws-cloudwatch-log-metric-filter-coverage'], - service: 'cloudwatch', - }), - ], - { mode: 'regions', regions: ['us-east-1'] }, - ); - - expect(mockedBuildAwsDiscoveryCatalog).toHaveBeenCalledWith({ mode: 'regions', regions: ['us-east-1'] }, [ - 'logs:log-group', - ]); - expect(mockedHydrateAwsCloudWatchLogMetricFilterCoverage).toHaveBeenCalledWith( - [catalog.resources[7]], - loadContextMatcher, - ); - expect(result.resources.get('aws-cloudwatch-log-metric-filter-coverage')).toEqual([ - { - accountId: '123456789012', - logGroupName: '/aws/lambda/app', - metricFilterCount: 0, - region: 'us-east-1', - }, - ]); - }); - it('loads only the S3 hydrator when active rules require only S3 bucket analyses', async () => { mockedBuildAwsDiscoveryCatalog.mockResolvedValue(catalog); mockedHydrateAwsS3BucketAnalyses.mockResolvedValue([ @@ -2866,7 +2799,7 @@ describe('discoverAwsResources', () => { ], searchRegion: 'us-east-1', }); - mockedHydrateAwsCloudWatchLogMetricFilterCoverage.mockImplementation(async (resources) => { + mockedHydrateAwsCloudWatchLogGroupRecentStreamActivity.mockImplementation(async (resources) => { const region = resources[0]?.region; if (region === 'eu-west-1') { const accessDeniedCause = Object.assign(new Error('Access denied by SCP.'), { @@ -2878,7 +2811,7 @@ describe('discoverAwsResources', () => { }); throw new Error( - 'Amazon CloudWatch Logs DescribeMetricFilters failed in eu-west-1 with AccessDeniedException: Access denied by SCP. Request ID: req-log-groups.', + 'Amazon CloudWatch Logs DescribeLogStreams failed in eu-west-1 with AccessDeniedException: Access denied by SCP. Request ID: req-log-groups.', { cause: accessDeniedCause, }, @@ -2888,7 +2821,6 @@ describe('discoverAwsResources', () => { return [ { accountId: '123456789012', - hasMetricFilters: true, logGroupArn: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/app-us-east-1', logGroupName: '/aws/lambda/app-us-east-1', region: 'us-east-1', @@ -2899,17 +2831,16 @@ describe('discoverAwsResources', () => { const result = await discoverAwsResources( [ createRule({ - discoveryDependencies: ['aws-cloudwatch-log-metric-filter-coverage'], + discoveryDependencies: ['aws-cloudwatch-log-group-recent-stream-activity'], service: 'cloudwatch', }), ], { mode: 'regions', regions: ['us-east-1', 'eu-west-1'] }, ); - expect(result.resources.get('aws-cloudwatch-log-metric-filter-coverage')).toEqual([ + expect(result.resources.get('aws-cloudwatch-log-group-recent-stream-activity')).toEqual([ { accountId: '123456789012', - hasMetricFilters: true, logGroupArn: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/app-us-east-1', logGroupName: '/aws/lambda/app-us-east-1', region: 'us-east-1', @@ -2919,7 +2850,7 @@ describe('discoverAwsResources', () => { { code: 'AccessDeniedException', details: - 'Amazon CloudWatch Logs DescribeMetricFilters failed in eu-west-1 with AccessDeniedException: Access denied by SCP. Request ID: req-log-groups.', + 'Amazon CloudWatch Logs DescribeLogStreams failed in eu-west-1 with AccessDeniedException: Access denied by SCP. Request ID: req-log-groups.', message: 'Skipped cloudwatch discovery in eu-west-1 because access is denied by a service control policy (SCP).', provider: 'aws', @@ -2946,17 +2877,17 @@ describe('discoverAwsResources', () => { ], searchRegion: 'eu-central-1', }); - mockedHydrateAwsCloudWatchLogMetricFilterCoverage.mockRejectedValue( + mockedHydrateAwsCloudWatchLogGroupRecentStreamActivity.mockRejectedValue( Object.assign( new Error( - 'Amazon CloudWatch Logs DescribeMetricFilters failed in eu-central-1 with ThrottlingException: Rate exceeded Request ID: req-metric-filters.', + 'Amazon CloudWatch Logs DescribeLogStreams failed in eu-central-1 with ThrottlingException: Rate exceeded Request ID: req-log-streams.', ), { code: 'ThrottlingException', name: 'ThrottlingException', $metadata: { httpStatusCode: 400, - requestId: 'req-metric-filters', + requestId: 'req-log-streams', }, }, ), @@ -2965,19 +2896,19 @@ describe('discoverAwsResources', () => { const result = await discoverAwsResources( [ createRule({ - discoveryDependencies: ['aws-cloudwatch-log-metric-filter-coverage'], + discoveryDependencies: ['aws-cloudwatch-log-group-recent-stream-activity'], service: 'cloudwatch', }), ], { mode: 'regions', regions: ['eu-central-1'] }, ); - expect(result.resources.get('aws-cloudwatch-log-metric-filter-coverage')).toEqual([]); + expect(result.resources.get('aws-cloudwatch-log-group-recent-stream-activity')).toEqual([]); expect(result.diagnostics).toEqual([ { code: 'ThrottlingException', details: - 'Amazon CloudWatch Logs DescribeMetricFilters failed in eu-central-1 with ThrottlingException: Rate exceeded Request ID: req-metric-filters.', + 'Amazon CloudWatch Logs DescribeLogStreams failed in eu-central-1 with ThrottlingException: Rate exceeded Request ID: req-log-streams.', message: 'Skipped cloudwatch discovery in eu-central-1 because AWS throttled the required dataset after retrying.', provider: 'aws', diff --git a/packages/sdk/test/providers/aws-static.test.ts b/packages/sdk/test/providers/aws-static.test.ts index d3d8ece..7c3877e 100644 --- a/packages/sdk/test/providers/aws-static.test.ts +++ b/packages/sdk/test/providers/aws-static.test.ts @@ -388,72 +388,6 @@ describe('loadAwsStaticResources', () => { ]); }); - it('loads API Gateway stages for Terraform and CloudFormation resources', async () => { - mockParsedResources([ - createIaCResource({ - type: 'aws_api_gateway_stage', - name: 'prod', - attributeLocations: { - cache_cluster_enabled: { - path: 'main.tf', - line: 6, - column: 3, - }, - }, - attributes: { - rest_api_id: 'a1b2c3d4', - stage_name: 'prod', - cache_cluster_enabled: false, - }, - }), - createIaCResource({ - type: 'AWS::ApiGateway::Stage', - name: 'ProdStage', - attributeLocations: { - 'Properties.CacheClusterEnabled': { - path: 'template.yaml', - line: 9, - column: 7, - }, - }, - attributes: { - Properties: { - StageName: 'prod', - RestApiId: 'a1b2c3d4', - CacheClusterEnabled: true, - }, - }, - }), - ]); - - const result = await loadAwsStaticResources('/tmp/iac', [ - createRule({ - staticDependencies: ['aws-apigateway-stages'], - }), - ]); - - expect(result.resources.get('aws-apigateway-stages')).toEqual([ - { - cacheClusterEnabled: false, - location: { - path: 'main.tf', - line: 6, - column: 3, - }, - resourceId: 'aws_api_gateway_stage.prod', - }, - { - cacheClusterEnabled: true, - location: { - path: 'template.yaml', - line: 9, - column: 7, - }, - resourceId: 'ProdStage', - }, - ]); - }); - it('loads CloudFront distributions and applies the default price class when omitted', async () => { mockParsedResources([ createIaCResource({ @@ -2225,66 +2159,6 @@ describe('aws static dataset registry', () => { ]); }); - it('normalizes Lambda provisioned concurrency resources for Terraform and CloudFormation', () => { - const definition = getAwsStaticDatasetDefinition('aws-lambda-provisioned-concurrency'); - - expect( - definition?.load([ - createIaCResource({ - type: 'aws_lambda_provisioned_concurrency_config', - name: 'worker', - attributeLocations: { - provisioned_concurrent_executions: { - path: 'main.tf', - line: 7, - column: 3, - }, - }, - attributes: { - provisioned_concurrent_executions: 5, - }, - }), - createIaCResource({ - type: 'AWS::Lambda::Alias', - name: 'WorkerAlias', - attributeLocations: { - 'Properties.ProvisionedConcurrencyConfig.ProvisionedConcurrentExecutions': { - path: 'template.yaml', - line: 14, - column: 7, - }, - }, - attributes: { - Properties: { - ProvisionedConcurrencyConfig: { - ProvisionedConcurrentExecutions: 12, - }, - }, - }, - }), - ]), - ).toEqual([ - { - location: { - path: 'main.tf', - line: 7, - column: 3, - }, - provisionedConcurrentExecutions: 5, - resourceId: 'aws_lambda_provisioned_concurrency_config.worker', - }, - { - location: { - path: 'template.yaml', - line: 14, - column: 7, - }, - provisionedConcurrentExecutions: 12, - resourceId: 'WorkerAlias', - }, - ]); - }); - it('normalizes RDS Performance Insights settings for Terraform and CloudFormation', () => { const definition = getAwsStaticDatasetDefinition('aws-rds-instances'); diff --git a/packages/sdk/test/scanner.test.ts b/packages/sdk/test/scanner.test.ts index 093129d..1256787 100644 --- a/packages/sdk/test/scanner.test.ts +++ b/packages/sdk/test/scanner.test.ts @@ -564,7 +564,7 @@ describe('CloudBurnClient', () => { { code: 'ThrottlingException', details: - 'Amazon CloudWatch Logs DescribeMetricFilters failed in us-east-1 with ThrottlingException: Rate exceeded Request ID: req-metric-filters.', + 'Amazon CloudWatch Logs DescribeLogStreams failed in us-east-1 with ThrottlingException: Rate exceeded Request ID: req-log-streams.', message: 'Skipped cloudwatch discovery in us-east-1 because AWS throttled the required dataset after retrying.', provider: 'aws', @@ -588,12 +588,12 @@ describe('CloudBurnClient', () => { } as never), unavailableDatasets: new Map([ [ - 'aws-cloudwatch-log-metric-filter-coverage', + 'aws-cloudwatch-log-group-recent-stream-activity', [ { code: 'ThrottlingException', details: - 'Amazon CloudWatch Logs DescribeMetricFilters failed in us-east-1 with ThrottlingException: Rate exceeded Request ID: req-metric-filters.', + 'Amazon CloudWatch Logs DescribeLogStreams failed in us-east-1 with ThrottlingException: Rate exceeded Request ID: req-log-streams.', message: 'Skipped cloudwatch discovery in us-east-1 because AWS throttled the required dataset after retrying.', provider: 'aws' as const, @@ -612,7 +612,7 @@ describe('CloudBurnClient', () => { const result = await scanner.discover({ config: { discovery: { - enabledRules: ['CLDBRN-AWS-CLOUDWATCH-3'], + enabledRules: ['CLDBRN-AWS-CLOUDWATCH-2'], }, iac: {}, }, @@ -628,7 +628,7 @@ describe('CloudBurnClient', () => { { code: 'ThrottlingException', details: - 'Amazon CloudWatch Logs DescribeMetricFilters failed in us-east-1 with ThrottlingException: Rate exceeded Request ID: req-metric-filters.', + 'Amazon CloudWatch Logs DescribeLogStreams failed in us-east-1 with ThrottlingException: Rate exceeded Request ID: req-log-streams.', message: 'Skipped cloudwatch discovery in us-east-1 because AWS throttled the required dataset after retrying.', provider: 'aws', @@ -639,11 +639,11 @@ describe('CloudBurnClient', () => { }, { details: - 'Amazon CloudWatch Logs DescribeMetricFilters failed in us-east-1 with ThrottlingException: Rate exceeded Request ID: req-metric-filters.', + 'Amazon CloudWatch Logs DescribeLogStreams failed in us-east-1 with ThrottlingException: Rate exceeded Request ID: req-log-streams.', message: - 'Skipped rule CLDBRN-AWS-CLOUDWATCH-3 because required discovery datasets were unavailable: aws-cloudwatch-log-metric-filter-coverage.', + 'Skipped rule CLDBRN-AWS-CLOUDWATCH-2 because required discovery datasets were unavailable: aws-cloudwatch-log-group-recent-stream-activity.', provider: 'aws', - ruleId: 'CLDBRN-AWS-CLOUDWATCH-3', + ruleId: 'CLDBRN-AWS-CLOUDWATCH-2', service: 'cloudwatch', source: 'discovery', status: 'skipped', @@ -655,8 +655,8 @@ describe('CloudBurnClient', () => { expect.objectContaining({ findingCount: 0, reason: - 'Skipped rule CLDBRN-AWS-CLOUDWATCH-3 because required discovery datasets were unavailable: aws-cloudwatch-log-metric-filter-coverage.', - ruleId: 'CLDBRN-AWS-CLOUDWATCH-3', + 'Skipped rule CLDBRN-AWS-CLOUDWATCH-2 because required discovery datasets were unavailable: aws-cloudwatch-log-group-recent-stream-activity.', + ruleId: 'CLDBRN-AWS-CLOUDWATCH-2', status: 'not_applicable', }), ], @@ -1310,7 +1310,7 @@ describe('CloudBurnClient', () => { }); }); - it('returns static API Gateway, CloudFront, and CloudWatch findings from mixed IaC resources', async () => { + it('returns static CloudFront and CloudWatch findings from mixed IaC resources', async () => { const scanner = new CloudBurnClient(); const fixturePath = fileURLToPath(new URL('./fixtures/iac-config-mixed', import.meta.url)); @@ -1321,23 +1321,6 @@ describe('CloudBurnClient', () => { { provider: 'aws', rules: [ - { - ruleId: 'CLDBRN-AWS-APIGATEWAY-1', - service: 'apigateway', - severity: 'medium', - source: 'iac', - message: 'API Gateway REST API stages should enable caching when stage caching is available.', - findings: [ - { - resourceId: 'aws_api_gateway_stage.prod', - location: { - path: 'main.tf', - line: 4, - column: 3, - }, - }, - ], - }, { ruleId: 'CLDBRN-AWS-CLOUDFRONT-1', service: 'cloudfront', @@ -1635,32 +1618,6 @@ describe('CloudBurnClient', () => { }, ], }, - { - ruleId: 'CLDBRN-AWS-S3-5', - service: 's3', - severity: 'low', - source: 'iac', - message: - 'S3 buckets without any storage-class transition should enable Intelligent-Tiering when access patterns are unknown.', - findings: [ - { - resourceId: 'aws_s3_bucket.missing_lifecycle', - location: { - path: 'main.tf', - line: 1, - column: 1, - }, - }, - { - resourceId: 'MissingLifecycleBucket', - location: { - path: 'template.yaml', - line: 2, - column: 3, - }, - }, - ], - }, ], }, ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d87da36..7a6d88c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,9 +99,6 @@ importers: packages/sdk: dependencies: - '@aws-sdk/client-api-gateway': - specifier: ^3.1015.0 - version: 3.1085.0 '@aws-sdk/client-application-auto-scaling': specifier: ^3.1009.0 version: 3.1085.0 @@ -218,10 +215,6 @@ packages: resolution: {integrity: sha512-EKnvkXSmz3IpA99tCNuI+dLFXyZyClSm8zns9sB/elvkU+MTuomAs6toJMPMBf98/fICG/urXDkzGz0/c3yyAQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/client-api-gateway@3.1085.0': - resolution: {integrity: sha512-L4xeygPep9iiT9Izy3TTlxTdMZNKHDRxk+uG9XpUqyKJzzno7gQ7jcibwBScKxofImn89kVu9Fkim1zm3mAjRA==} - engines: {node: '>=20.0.0'} - '@aws-sdk/client-application-auto-scaling@3.1085.0': resolution: {integrity: sha512-twnCLDOMQpArcZw5eBWUw8euli24aMN2aJHrIlxnNVgK4eM+6tPHh6pbcLuUd8P9hS2Myr32ytEsEznpMBFXDw==} engines: {node: '>=20.0.0'} @@ -370,10 +363,6 @@ packages: resolution: {integrity: sha512-FoEkpD2A8haYWNT4wp8zW1x0Hc+NyxcnKJ2Hf4jX8EI5R4ybo1LRI3ql7iuFX1MTyklZx9hum9dPkq6R46pQMw==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-sdk-api-gateway@3.972.22': - resolution: {integrity: sha512-syOvziZ3GmBN2bbxQV8xPIobZu9s4rbg2BF+I8nf84pmWc8hy04FquqvIS91qto4v2ywdoiFx7COyzfRWAh0MA==} - engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-sdk-ec2@3.972.45': resolution: {integrity: sha512-wcSIL3HWcEQQkf+eKbyhwqSML23uTUI2zAYexRRa0akL74pnq5olKh06a0kGHWsn6SUHcw9zy9dExA8UuJAUCw==} engines: {node: '>=20.0.0'} @@ -1954,18 +1943,6 @@ snapshots: '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/client-api-gateway@3.1085.0': - dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/credential-provider-node': 3.972.66 - '@aws-sdk/middleware-sdk-api-gateway': 3.972.22 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 - '@smithy/fetch-http-handler': 5.6.5 - '@smithy/node-http-handler': 4.9.5 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - '@aws-sdk/client-application-auto-scaling@3.1085.0': dependencies: '@aws-sdk/core': 3.975.1 @@ -2366,13 +2343,6 @@ snapshots: '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-api-gateway@3.972.22': - dependencies: - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 - '@smithy/types': 4.16.1 - tslib: 2.8.1 - '@aws-sdk/middleware-sdk-ec2@3.972.45': dependencies: '@aws-sdk/core': 3.975.1