diff --git a/README.md b/README.md index 30ce17098c..caf95d646c 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ See [RULES](./RULES.md) for more information on all the available packs. 3. [NIST 800-53 rev 4](./RULES.md#nist-800-53-rev-4) 4. [NIST 800-53 rev 5](./RULES.md#nist-800-53-rev-5) 5. [PCI DSS 3.2.1](./RULES.md#pci-dss-321) +6. [Serverless](./RULES.md#serverless) [RULES](./RULES.md) also includes a collection of [additional rules](./RULES.md#additional-rules) that are not currently included in any of the pre-built NagPacks, but are still available for inclusion in custom NagPacks. diff --git a/RULES.md b/RULES.md index 5d28cd202d..fd94fb623d 100644 --- a/RULES.md +++ b/RULES.md @@ -319,7 +319,7 @@ The [Operational Best Practices for NIST 800-53 rev 4](https://docs.aws.amazon.c | [NIST.800.53.R4-ALBWAFEnabled](https://docs.aws.amazon.com/config/latest/developerguide/alb-waf-enabled.html) | The ALB is not associated with AWS WAFv2 web ACL. | A WAF helps to protect your web applications or APIs against common web exploits. These web exploits may affect availability, compromise security, or consume excessive resources within your environment. | SC-7, SI-4(a)(b)(c) | | [NIST.800.53.R4-APIGWCacheEnabledAndEncrypted](https://docs.aws.amazon.com/config/latest/developerguide/api-gw-cache-enabled-and-encrypted.html) | The API Gateway stage does not have caching enabled and encrypted for all methods. | To help protect data at rest, ensure encryption is enabled for your API Gateway stage's cache. Because sensitive data can be captured for the API method, enable encryption at rest to help protect that data. | SC-13, SC-28 | | [NIST.800.53.R4-APIGWExecutionLoggingEnabled](https://docs.aws.amazon.com/config/latest/developerguide/api-gw-execution-logging-enabled.html) | The API Gateway stage does not have execution logging enabled for all methods. | API Gateway logging displays detailed views of users who accessed the API and the way they accessed the API. This insight enables visibility of user activities. | AU-2(a)(d), AU-3, AU-12(a)(c) | -| [NIST.800.53.R4-AutoScalingGroupELBHealthCheckRequired](https://docs.aws.amazon.com/config/latest/developerguide/autoscaling-group-elb-healthcheck-required.html) | The Auto Scaling group (which is associated with a load balancer) does not utilize ELB health checks. | The Elastic Load Balancer (ELB) health checks for Amazon Elastic Compute Cloud (Amazon EC2) Auto Scaling groups support maintenance of adequate capacity and availability. | SC-5 | +| [NIST.800.53.R4-AutoScalingGroupELBHealthCheckRequired](https://docs.aws.amazon.com/config/latest/developerguide/autoscaling-group-elb-healthcheck-required.html) | The Auto Scaling group (which is associated with a load balancer) does not utilize ELB health checks. | The Elastic Load Balancer (ELB) health checks for Amazon Elastic Compute Cloud (Amazon EC2) Auto Scaling groups support maintenance of adequate capacity and availability. | SC-5 | | [NIST.800.53.R4-CloudTrailCloudWatchLogsEnabled](https://docs.aws.amazon.com/config/latest/developerguide/cloud-trail-cloud-watch-logs-enabled.html) | The trail does not have CloudWatch logs enabled. | Use Amazon CloudWatch to centrally collect and manage log event activity. Inclusion of AWS CloudTrail data provides details of API call activity within your AWS account. | AC-2(4), AC-2(g), AU-2(a)(d), AU-3, AU-6(1)(3), AU-7(1), AU-12(a)(c), CA-7(a)(b), SI-4(2), SI-4(4), SI-4(5), SI-4(a)(b)(c) | | [NIST.800.53.R4-CloudTrailEncryptionEnabled](https://docs.aws.amazon.com/config/latest/developerguide/cloud-trail-encryption-enabled.html) | The trail does not have encryption enabled. | Because sensitive data may exist and to help protect data at rest, ensure encryption is enabled for your AWS CloudTrail trails. | AU-9, SC-13, SC-28 | | [NIST.800.53.R4-CloudTrailLogFileValidationEnabled](https://docs.aws.amazon.com/config/latest/developerguide/cloud-trail-log-file-validation-enabled.html) | The trail does not have log file validation enabled. | Utilize AWS CloudTrail log file validation to check the integrity of CloudTrail logs. Log file validation helps determine if a log file was modified or deleted or unchanged after CloudTrail delivered it. This feature is built using industry standard algorithms: SHA-256 for hashing and SHA-256 with RSA for digital signing. This makes it computationally infeasible to modify, delete or forge CloudTrail log files without detection. | AU-9, SC-13, SC-28 | @@ -447,7 +447,7 @@ The [Operational Best Practices for NIST 800-53 rev 5](https://docs.aws.amazon.c | [NIST.800.53.R5-APIGWCacheEnabledAndEncrypted](https://docs.aws.amazon.com/config/latest/developerguide/api-gw-cache-enabled-and-encrypted.html) | The API Gateway stage does not have caching enabled and encrypted for all methods. | To help protect data at rest, ensure encryption is enabled for your API Gateway stage's cache. Because sensitive data can be captured for the API method, enable encryption at rest to help protect that data. | AU-9(3), CP-9d, SC-8(3), SC-8(4), SC-13a, SC-28(1), SI-19(4) | | [NIST.800.53.R5-APIGWExecutionLoggingEnabled](https://docs.aws.amazon.com/config/latest/developerguide/api-gw-execution-logging-enabled.html) | The API Gateway stage does not have execution logging enabled for all methods. | API Gateway logging displays detailed views of users who accessed the API and the way they accessed the API. This insight enables visibility of user activities. | AC-4(26), AU-2b, AU-3a, AU-3b, AU-3c, AU-3d, AU-3e, AU-3f, AU-6(3), AU-6(4), AU-6(6), AU-6(9), AU-8b, AU-10, AU-12a, AU-12c, AU-12(1), AU-12(2), AU-12(3), AU-12(4), AU-14a, AU-14b, AU-14b, AU-14(3), CA-7b, CM-5(1)(b), IA-3(3)(b), MA-4(1)(a), PM-14a.1, PM-14b, PM-31, SC-7(9)(b), SI-4(17), SI-7(8) | | [NIST.800.53.R5-APIGWSSLEnabled](https://docs.aws.amazon.com/config/latest/developerguide/api-gw-ssl-enabled.html) | The API Gateway REST API stage is not configured with SSL certificates. | Ensure Amazon API Gateway REST API stages are configured with SSL certificates to allow backend systems to authenticate that requests originate from API Gateway. | AC-4, AC-4(22), AC-17(2), AC-24(1), AU-9(3), CA-9b, IA-5(1)(c), PM-17b, SC-7(4)(b), SC-7(4)(g), SC-8, SC-8(1), SC-8(2), SC-8(3), SC-8(4), SC-8(5), SC-13a, SC-23, SI-1a.2, SI-1a.2, SI-1c.2 | -| [NIST.800.53.R5-AutoScalingGroupELBHealthCheckRequired](https://docs.aws.amazon.com/config/latest/developerguide/autoscaling-group-elb-healthcheck-required.html) | The Auto Scaling group (which is associated with a load balancer) does not utilize ELB health checks. | The Elastic Load Balancer (ELB) health checks for Amazon Elastic Compute Cloud (Amazon EC2) Auto Scaling groups support maintenance of adequate capacity and availability. The load balancer periodically sends pings, attempts connections, or sends requests to test Amazon EC2 instances health in an auto-scaling group. If an instance is not reporting back, traffic is sent to a new Amazon EC2 instance. | AU-12(3), AU-14a, AU-14b, CA-2(2), CA-7, CA-7b, CM-6a, CM-9b, PM-14a.1, PM-14b, PM-31, SC-6, SC-36(1)(a), SI-2a | +| [NIST.800.53.R5-AutoScalingGroupELBHealthCheckRequired](https://docs.aws.amazon.com/config/latest/developerguide/autoscaling-group-elb-healthcheck-required.html) | The Auto Scaling group (which is associated with a load balancer) does not utilize ELB health checks. | The Elastic Load Balancer (ELB) health checks for Amazon Elastic Compute Cloud (Amazon EC2) Auto Scaling groups support maintenance of adequate capacity and availability. The load balancer periodically sends pings, attempts connections, or sends requests to test Amazon EC2 instances health in an auto-scaling group. If an instance is not reporting back, traffic is sent to a new Amazon EC2 instance. | AU-12(3), AU-14a, AU-14b, CA-2(2), CA-7, CA-7b, CM-6a, CM-9b, PM-14a.1, PM-14b, PM-31, SC-6, SC-36(1)(a), SI-2a | | [NIST.800.53.R5-AutoScalingLaunchConfigPublicIpDisabled](https://docs.aws.amazon.com/config/latest/developerguide/autoscaling-launch-config-public-ip-disabled.html) | The Auto Scaling launch configuration does not have public IP addresses disabled. | If you configure your Network Interfaces with a public IP address, then the associated resources to those Network Interfaces are reachable from the internet. EC2 resources should not be publicly accessible, as this may allow unintended access to your applications or servers. | AC-3, AC-4(21), CM-6a, SC-7(3) | | [NIST.800.53.R5-CloudTrailCloudWatchLogsEnabled](https://docs.aws.amazon.com/config/latest/developerguide/cloud-trail-cloud-watch-logs-enabled.html) | The trail does not have CloudWatch logs enabled. | Use Amazon CloudWatch to centrally collect and manage log event activity. Inclusion of AWS CloudTrail data provides details of API call activity within your AWS account. | AC-2(4), AC-3(1), AC-3(10), AC-4(26), AC-6(9), AU-2b, AU-3a, AU-3b, AU-3c, AU-3d, AU-3e, AU-3f, AU-4(1), AU-6(1), AU-6(3), AU-6(4), AU-6(5), AU-6(6), AU-6(9), AU-7(1), AU-8b, AU-9(7), AU-10, AU-12a, AU-12c, AU-12(1), AU-12(2), AU-12(3), AU-12(4), AU-14a, AU-14b, AU-14b, AU-14(3), AU-16, CA-7b, CM-5(1)(b), CM-6a, CM-9b, IA-3(3)(b), MA-4(1)(a), PM-14a.1, PM-14b, PM-31, SC-7(9)(b), SI-1(1)(c), SI-3(8)(b), SI-4(2), SI-4(17), SI-4(20), SI-7(8), SI-10(1)(c) | | [NIST.800.53.R5-CloudTrailEncryptionEnabled](https://docs.aws.amazon.com/config/latest/developerguide/cloud-trail-encryption-enabled.html) | The trail does not have encryption enabled. | Because sensitive data may exist and to help protect data at rest, ensure encryption is enabled for your AWS CloudTrail trails. | AU-9(3), CM-6a, CM-9b, CP-9d, SC-8(3), SC-8(4), SC-13a, SC-28(1), SI-19(4) | @@ -592,7 +592,7 @@ The [Operational Best Practices for PCI DSS 3.2.1](https://docs.aws.amazon.com/c | [PCI.DSS.321-APIGWCacheEnabledAndEncrypted](https://docs.aws.amazon.com/config/latest/developerguide/api-gw-cache-enabled-and-encrypted.html) | The API Gateway stage does not have caching enabled and encrypted for all methods. | To help protect data at rest, ensure encryption is enabled for your API Gateway stage's cache. Because sensitive data can be captured for the API method, enable encryption at rest to help protect that data. | 3.4 | | [PCI.DSS.321-APIGWExecutionLoggingEnabled](https://docs.aws.amazon.com/config/latest/developerguide/api-gw-execution-logging-enabled.html) | The API Gateway stage does not have execution logging enabled for all methods. | API Gateway logging displays detailed views of users who accessed the API and the way they accessed the API. This insight enables visibility of user activities. | 10.1, 10.3.1, 10.3.2, 10.3.3, 10.3.4, 10.3.5, 10.3.6, 10.5.4 | | [PCI.DSS.321-APIGWSSLEnabled](https://docs.aws.amazon.com/config/latest/developerguide/api-gw-ssl-enabled.html) | The API Gateway REST API stage is not configured with SSL certificates. | Ensure Amazon API Gateway REST API stages are configured with SSL certificates to allow backend systems to authenticate that requests originate from API Gateway. | 2.3, 4.1, 8.2.1 | -| [PCI.DSS.321-AutoScalingGroupELBHealthCheckRequired](https://docs.aws.amazon.com/config/latest/developerguide/autoscaling-group-elb-healthcheck-required.html) | The Auto Scaling group (which is associated with a load balancer) does not utilize ELB health checks. | The Elastic Load Balancer (ELB) health checks for Amazon Elastic Compute Cloud (Amazon EC2) Auto Scaling groups support maintenance of adequate capacity and availability. The load balancer periodically sends pings, attempts connections, or sends requests to test Amazon EC2 instances health in an auto-scaling group. If an instance is not reporting back, traffic is sent to a new Amazon EC2 instance. | 2.2 | +| [PCI.DSS.321-AutoScalingGroupELBHealthCheckRequired](https://docs.aws.amazon.com/config/latest/developerguide/autoscaling-group-elb-healthcheck-required.html) | The Auto Scaling group (which is associated with a load balancer) does not utilize ELB health checks. | The Elastic Load Balancer (ELB) health checks for Amazon Elastic Compute Cloud (Amazon EC2) Auto Scaling groups support maintenance of adequate capacity and availability. The load balancer periodically sends pings, attempts connections, or sends requests to test Amazon EC2 instances health in an auto-scaling group. If an instance is not reporting back, traffic is sent to a new Amazon EC2 instance. | 2.2 | | [PCI.DSS.321-AutoScalingLaunchConfigPublicIpDisabled](https://docs.aws.amazon.com/config/latest/developerguide/autoscaling-launch-config-public-ip-disabled.html) | The Auto Scaling launch configuration does not have public IP addresses disabled. | If you configure your Network Interfaces with a public IP address, then the associated resources to those Network Interfaces are reachable from the internet. EC2 resources should not be publicly accessible, as this may allow unintended access to your applications or servers. | 1.2, 1.2.1, 1.3, 1.3.1, 1.3.2, 1.3.4, 1.3.6, 2.2.2 | | [PCI.DSS.321-CloudTrailCloudWatchLogsEnabled](https://docs.aws.amazon.com/config/latest/developerguide/cloud-trail-cloud-watch-logs-enabled.html) | The trail does not have CloudWatch logs enabled. | Use Amazon CloudWatch to centrally collect and manage log event activity. Inclusion of AWS CloudTrail data provides details of API call activity within your AWS account. | 2.2, 10.1, 10.2.1, 10.2.2, 10.2.3, 10.2.5, 10.3.1, 10.3.2, 10.3.3, 10.3.4, 10.3.5, 10.3.6, 10.5.3, 10.5.4 | | [PCI.DSS.321-CloudTrailEncryptionEnabled](https://docs.aws.amazon.com/config/latest/developerguide/cloud-trail-encryption-enabled.html) | The trail does not have encryption enabled. | Because sensitive data may exist and to help protect data at rest, ensure encryption is enabled for your AWS CloudTrail trails. | 2.2, 3.4, 10.5 | @@ -703,6 +703,39 @@ A collection of community rules that are not currently included in any of the pr | --------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | LambdaFunctionUrlAuth | The Lambda Function URL allows for public, unauthenticated access. | AWS Lambda Function URLs allow you to invoke your function via a HTTPS end-point, setting the authentication to NONE allows anyone on the internet to invoke your function. | + +## Serverless + +The [Serverless Rules](https://awslabs.github.io/serverless-rules/) are a compilation of rules to validate infrastructure-as-code template against recommended practices. + +### Warnings + +| Rule ID | Cause | Explanation | +| ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [LambdaTracing](https://awslabs.github.io/serverless-rules/rules/lambda/tracing/) | Lambda function does not have X-Ray tracing enabled. | AWS Lambda can emit traces to AWS X-Ray, which enables visualizing service maps for faster troubleshooting. X-Ray tracing provides insights into the performance and behavior of your Lambda functions, helping to identify bottlenecks, errors, and dependencies. Enabling tracing allows you to track requests as they traverse through your serverless applications, making it easier to debug and optimize your functions. Consider enabling X-Ray tracing. | +| [StarPermissions](https://awslabs.github.io/serverless-rules/rules/lambda/star_permissions/) | IAM role has overly permissive policies. | IAM roles should follow the principle of least privilege. Avoid using wildcard (*) permissions in IAM policies attached to Lambda roles. Instead, specify only the permissions required for the function to operate. This reduces the potential impact if the function is compromised. Review and tighten the IAM permissions for your Lambda functions. | +| [LambdaDefaultMemorySize](https://awslabs.github.io/serverless-rules/rules/lambda/default_memory_size/) | Lambda function does not specify a memory size. | Lambda CPU power and costs are proportional to the amount of memory configured. You can use tools such as AWS Lambda Power Tuning to test your function at different memory settings to find the one that matches your cost and performance requirements the best. | +| [CloudWatchLogGroupRetentionPeriod](https://docs.aws.amazon.com/config/latest/developerguide/cw-loggroup-retention-period-check.html) | The CloudWatch Log Group does not have an explicit retention period configured. | By default, CloudWatch log groups created by Lambda functions have an unlimited retention time. For cost optimization purposes, you should set a retention duration on all log groups. For log archival, export and set cost-effective storage classes that best suit your needs. | +| [LambdaFunctionDefaultTimeout](https://awslabs.github.io/serverless-rules/rules/lambda/default_timeout/) | Lambda function uses the default timeout. | The default timeout for Lambda functions is 3 seconds, which may not be sufficient for many use cases. Setting an appropriate timeout helps prevent unexpected function terminations and ensures your function has enough time to complete its tasks. Consider the nature of your function's operations and set a timeout that balances between allowing sufficient execution time and avoiding unnecessarily long-running functions. | +| [LambdaLatestVersion](https://awslabs.github.io/serverless-rules/rules/lambda/end_of_life_runtime/) | Lambda function is not using the latest runtime version. | Using the latest runtime version ensures that your Lambda function has access to the most recent features, performance improvements, and security updates. It's important to regularly update your Lambda functions to use the latest runtime versions to maintain optimal performance and security. | +| [APIGWStructuredLogging](https://awslabs.github.io/serverless-rules/rules/apigateway/structured_logging/) | The API Gateway stage does not have structured logging enabled. | API Gateway can emit structured logs to CloudWatch Logs. Structured logging provides a consistent and machine-readable format for log entries, making it easier to search, analyze, and process log data. Enable structured logging to improve the observability and troubleshooting capabilities of your API Gateway deployments. | +| [StepFunctionStateMachineXray](https://awslabs.github.io/serverless-rules/rules/stepfunctions/xray/) | The Step Functions state machine does not have X-Ray tracing enabled. | AWS Step Functions can emit traces to AWS X-Ray, which enables visualizing service maps for faster troubleshooting. X-Ray tracing provides insights into the execution of your state machines, helping to identify bottlenecks, errors, and dependencies. Enabling tracing allows you to track executions as they traverse through your serverless workflows, making it easier to debug and optimize your state machines. Consider enabling X-Ray tracing. | +| [AppSyncTracing](https://awslabs.github.io/serverless-rules/rules/appsync/tracing/) | The AppSync API does not have X-Ray tracing enabled. | AWS AppSync can emit traces to AWS X-Ray, which enables visualizing service maps for faster troubleshooting. X-Ray tracing provides insights into the execution of your GraphQL APIs, helping to identify bottlenecks, errors, and dependencies. Enabling tracing allows you to track requests as they traverse through your serverless applications, making it easier to debug and optimize your AppSync APIs. Consider enabling X-Ray tracing for improved observability. | + +### Errors + +| Rule ID | Cause | Explanation | +| -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [LambdaEventSourceMappingDestination](https://awslabs.github.io/serverless-rules/rules/lambda/eventsourcemapping_failure_destination/) | The Lambda Event Source Mapping does not have a destination configured for failed invocations. | Configuring a destination for failed invocations in Lambda Event Source Mappings allows you to capture and process events that fail to be processed by your Lambda function. This helps in monitoring, debugging, and implementing retry mechanisms for failed events, improving the reliability and observability of your serverless applications. | +| [LambdaAsyncFailureDestination](https://awslabs.github.io/serverless-rules/rules/lambda/async_failure_destination/) | The Lambda function does not have a failure destination for asynchronous invocations. | When a Lambda function is invoked asynchronously (e.g., by S3, SNS, or EventBridge), it's important to configure a failure destination. This allows you to capture and handle events that fail processing, improving the reliability and observability of your serverless applications. | +| [LambdaDefaultMemorySize](https://awslabs.github.io/serverless-rules/rules/lambda/default_memory_size/) | Lambda function does not specify a memory size. | Lambda CPU power and costs are proportional to the amount of memory configured. You can use tools such as AWS Lambda Power Tuning to test your function at different memory settings to find the one that matches your cost and performance requirements the best. | | +| [APIGWAccessLogging](https://awslabs.github.io/serverless-rules/rules/api_gateway/logging/) | The API Gateway stage does not have access logging enabled. | API Gateway provides access logging for API stages. Enable access logging on your API stages to monitor API requests and responses. | +| [SQSRedrivePolicy](https://awslabs.github.io/serverless-rules/rules/sqs/redrive_policy/) | The SQS queue does not have a redrive policy configured. | Configuring a redrive policy on an SQS queue allows you to define how many times SQS will make messages available for consumers before sending them to a dead-letter queue. This helps in managing message processing failures and provides a mechanism for handling problematic messages. | +| [SNSRedrivePolicy](https://awslabs.github.io/serverless-rules/rules/sns/sns-subscription-dlq/) | The SNS subscription does not have a redrive policy specified. | Configuring a redrive policy for SNS subscriptions helps manage message delivery failures by specifying how many times SNS will retry undeliverable messages before sending them to a dead-letter queue. This improves the reliability of your messaging system by providing a mechanism to handle and retry failed message deliveries. | +| [EventBusDLQ](https://awslabs.github.io/serverless-rules/rules/eventbridge/eventbridge-dlq/) | The EventBridge rule does not have a Dead-Letter Queue (DLQ) configured. | Configuring a Dead-Letter Queue (DLQ) for EventBridge rules helps manage failed event deliveries. When a rule's target fails to process an event, the DLQ captures these failed events, allowing for analysis, troubleshooting, and potential reprocessing. This improves the reliability and observability of your event-driven architectures by providing a safety net for handling delivery failures. | +| [APIGWDefaultThrottling](https://awslabs.github.io/serverless-rules/rules/api_gateway/default_throttling/) | The API Gateway stage is using default throttling settings. | API Gateway default throttling settings may not be suitable for all applications. Custom throttling limits help protect your backend systems from being overwhelmed with requests, ensure consistent performance. | +c + ## Footnotes 1: This rule is intentionally unimplemented. Server-side encryption at rest is enabled on all DynamoDB table data and [cannot be disabled](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/encryption.usagenotes.html#encryption.usagenotes.tabledata). diff --git a/src/packs/serverless.ts b/src/packs/serverless.ts new file mode 100644 index 0000000000..13cf025578 --- /dev/null +++ b/src/packs/serverless.ts @@ -0,0 +1,279 @@ +import { CfnResource } from 'aws-cdk-lib'; +import { IConstruct } from 'constructs'; +import { NagPack, NagPackProps } from '../nag-pack'; +import { NagMessageLevel } from '../nag-rules'; +import { + apigw, + appsync, + cloudwatch, + eventbridge, + iam, + lambda, + sns, + sqs, + stepfunctions, +} from '../rules'; + +/** + * Serverless Checks are a compilation of rules to validate infrastructure-as-code template against recommended practices. + * + */ +export class ServerlessChecks extends NagPack { + constructor(props?: NagPackProps) { + super(props); + this.packName = 'Serverless'; + } + public visit(node: IConstruct): void { + if (node instanceof CfnResource) { + this.checkLambda(node); + this.checkCloudwatch(node); + this.checkIAM(node); + this.checkApiGw(node); + this.checkAppSync(node); + this.checkEventBridge(node); + this.checkSNS(node); + this.checkSQS(node); + this.checkStepFunctions(node); + } + } + + /** + * Check Lambda Resources + * @param node the CfnResource to check + * @param ignores list of ignores for the resource + */ + private checkLambda(node: CfnResource) { + this.applyRule({ + info: 'The Lambda function should have tracing set to Tracing.ACTIVE.', + explanation: + 'When a Lambda function has ACTIVE tracing, Lambda automatically samples invocation requests, based on the sampling algorithm specified by X-Ray.', + level: NagMessageLevel.WARN, + rule: lambda.LambdaTracing, + node: node, + }); + + this.applyRule({ + info: 'Lambda Event Source Mappings have a destination configured for failed invocations.', + explanation: + 'Configuring a destination for failed invocations in Lambda Event Source Mappings allows you to capture and process events that fail to be processed by your Lambda function. This helps in monitoring, debugging, and implementing retry mechanisms for failed events, improving the reliability and observability of your serverless applications.', + level: NagMessageLevel.ERROR, + rule: lambda.LambdaEventSourceMappingDestination, + node: node, + }); + + this.applyRule({ + info: 'Lambda functions utilize the latest available runtime version.', + explanation: + "Using the latest runtime version ensures that your Lambda function has access to the most recent features, performance improvements, and security updates. It's important to regularly update your Lambda functions to use the latest runtime versions to maintain optimal performance and security.", + level: NagMessageLevel.WARN, + rule: lambda.LambdaLatestVersion, + node: node, + }); + + this.applyRule({ + info: 'Lambda functions have a configured failure destination for asynchronous invocations.', + explanation: + "When a Lambda function is invoked asynchronously (e.g., by S3, SNS, or EventBridge), it's important to configure a failure destination. This allows you to capture and handle events that fail processing, improving the reliability and observability of your serverless applications.", + level: NagMessageLevel.ERROR, + rule: lambda.LambdaAsyncFailureDestination, + node: node, + }); + + this.applyRule({ + info: 'Lambda functions have an explicit memory value configured', + explanation: + "Lambda allocates CPU power in proportion to the amount of memory configured. By default, your functions have 128 MB of memory allocated. You can increase that value up to 10 GB. With more CPU resources, your Lambda function's duration might decrease. You can use tools such as AWS Lambda Power Tuning to test your function at different memory settings to find the one that matches your cost and performance requirements the best.", + level: NagMessageLevel.ERROR, + rule: lambda.LambdaDefaultMemorySize, + node: node, + }); + + this.applyRule({ + info: 'Lambda functions must have an explicitly defined timeout value.', + explanation: + 'Lambda functions have a default timeout of 3 seconds. If your timeout value is too short, Lambda might terminate invocations prematurely. On the other side, setting the timeout much higher than the average execution may cause functions to execute for longer upon code malfunction, resulting in higher costs and possibly reaching concurrency limits depending on how such functions are invoked. You can also use AWS Lambda Power Tuning to test your function at different timeout settings to find the one that matches your cost and performance requirements the best.', + level: NagMessageLevel.ERROR, + rule: lambda.LambdaDefaultTimeout, + node: node, + }); + + this.applyRule({ + info: 'Lambda functions have a dead letter target configured.', + explanation: + 'When a lambda function has the DeadLetterConfig property set, failed messages can be temporarily stored for review in an SQS queue or an SNS topic.', + level: NagMessageLevel.ERROR, + rule: lambda.LambdaDLQ, + node: node, + }); + + this.applyRule({ + info: 'Lambda functions are required to use the latest runtime version.', + explanation: + "Using the latest runtime version ensures that your Lambda function has access to the most recent features, performance improvements, and security updates. It's important to regularly update your Lambda functions to use the latest runtime versions to maintain optimal performance and security.", + level: NagMessageLevel.ERROR, + rule: lambda.LambdaLatestVersion, + node: node, + }); + + this.applyRule({ + info: 'Lambda Event Source Mappings must have a destination configured for failed invocations.', + explanation: + 'Configuring a destination for failed invocations in Lambda Event Source Mappings allows you to capture and process events that fail to be processed by your Lambda function. This helps in monitoring, debugging, and implementing retry mechanisms for failed events, improving the reliability and observability of your serverless applications.', + level: NagMessageLevel.ERROR, + rule: lambda.LambdaEventSourceMappingDestination, + node: node, + }); + } + + /** + * Check Lambda Resources + * @param node the CfnResource to check + * @param ignores list of ignores for the resource + */ + private checkIAM(node: CfnResource) { + this.applyRule({ + info: 'Lambda functions do not have overly permissive IAM roles.', + explanation: + 'Lambda functions should follow the principle of least privilege. Avoid using wildcard (*) permissions in IAM roles attached to Lambda functions. Instead, specify only the permissions required for the function to operate.', + level: NagMessageLevel.WARN, + rule: iam.IAMNoWildcardPermissions, + node: node, + }); + } + + /** + * Check Cloudwatch Resources + * @param node the CfnResource to check + * @param ignores list of ignores for the resource + */ + private checkCloudwatch(node: CfnResource) { + this.applyRule({ + info: 'CloudWatch Log Groups must have an explicit retention policy defined.', + explanation: + 'By default, logs are kept indefinitely and never expire. You can adjust the retention policy for each log group, keeping the indefinite retention, or choosing a retention period between one day and 10 years. For Lambda functions, this applies to their automatically created CloudWatch Log Groups.', + level: NagMessageLevel.WARN, + rule: cloudwatch.CloudWatchLogGroupRetentionPeriod, + node: node, + }); + } + + /** + * Check API Gateway Resources + * @param node the CfnResource to check + * @param ignores list of ignores for the resource + */ + private checkApiGw(node: CfnResource) { + this.applyRule({ + info: 'API Gateway stages have access logging enabled.', + explanation: + 'API Gateway provides access logging for API stages. Enable access logging on your API stages to monitor API requests and responses.', + level: NagMessageLevel.ERROR, + rule: apigw.APIGWAccessLogging, + node: node, + }); + this.applyRule({ + info: 'API Gateways have Tracing enabled.', + explanation: + 'Amazon API Gateway provides active tracing support for AWS X-Ray. Enable active tracing on your API stages to sample incoming requests and send traces to X-Ray.', + level: NagMessageLevel.ERROR, + rule: apigw.APIGWXrayEnabled, + node: node, + }); + this.applyRule({ + info: 'API Gateway logs are configured for the JSON format.', + explanation: + 'You can customize the log format that Amazon API Gateway uses to send logs. JSON Structured logging makes it easier to derive queries to answer arbitrary questions about the health of your application.', + level: NagMessageLevel.ERROR, + rule: apigw.APIGWStructuredLogging, + node: node, + }); + this.applyRule({ + info: 'API Gateway stages are not using default throttling setting.s', + explanation: + 'API Gateway default throttling settings may not be suitable for all applications. Custom throttling limits help protect your backend systems from being overwhelmed with requests, ensure consistent performance, and can be tailored to your specific use case.', + level: NagMessageLevel.ERROR, + rule: apigw.APIGWDefaultThrottling, + node: node, + }); + } + + /** + * Check AppSync Resources + * @param node the CfnResource to check + * @param ignores list of ignores for the resource + */ + private checkAppSync(node: CfnResource) { + this.applyRule({ + info: 'AppSync APIs have tracing enabled', + explanation: + 'AWS AppSync can emit traces to AWS X-Ray, which enables visualizing service maps for faster troubleshooting.', + level: NagMessageLevel.WARN, + rule: appsync.AppSyncTracing, + node: node, + }); + } + + /** + * Check EventBridge Resources + * @param node the CfnResource to check + * @param ignores list of ignores for the resource + */ + private checkEventBridge(node: CfnResource) { + this.applyRule({ + info: 'EventBridge targets have a DLQ configured.', + explanation: + "Configuring a Dead-Letter Queue (DLQ) for EventBridge rules helps manage failed event deliveries. When a rule's target fails to process an event, the DLQ captures these failed events, allowing for analysis, troubleshooting, and potential reprocessing. This improves the reliability and observability of your event-driven architectures by providing a safety net for handling delivery failures.", + level: NagMessageLevel.ERROR, + rule: eventbridge.EventBusDLQ, + node: node, + }); + } + + /** + * Check SNS Resources + * @param node the CfnResource to check + * @param ignores list of ignores for the resource + */ + private checkSNS(node: CfnResource) { + this.applyRule({ + info: 'SNS subscriptions have a redrive policy configured.', + explanation: + 'Configuring a redrive policy helps manage message delivery failures by sending undeliverable messages to a dead-letter queue.', + level: NagMessageLevel.ERROR, + rule: sns.SNSRedrivePolicy, + node: node, + }); + } + + /** + * Check SQS Resources + * @param node the CfnResource to check + * @param ignores list of ignores for the resource + */ + private checkSQS(node: CfnResource) { + this.applyRule({ + info: 'SQS queues have a redrive policy configured.', + explanation: + 'Configuring a redrive policy on an SQS queue allows you to define how many times SQS will make messages available for consumers before sending them to a dead-letter queue. This helps in managing message processing failures and provides a mechanism for handling problematic messages.', + level: NagMessageLevel.ERROR, + rule: sqs.SQSRedrivePolicy, + node: node, + }); + } + + /** + * Check StepFunctions Resources + * @param node the CfnResource to check + * @param ignores list of ignores for the resource + */ + private checkStepFunctions(node: CfnResource) { + this.applyRule({ + info: 'StepFunctions have X-Ray tracing configured.', + explanation: + 'AWS StepFunctions provides active tracing support for AWS X-Ray. Enable active tracing on your API stages to sample incoming requests and send traces to X-Ray.', + level: NagMessageLevel.ERROR, + rule: stepfunctions.StepFunctionStateMachineXray, + node: node, + }); + } +} diff --git a/src/rules/apigw/APIGWAccessLogging.ts b/src/rules/apigw/APIGWAccessLogging.ts index 02e4386e72..ef68527bcc 100644 --- a/src/rules/apigw/APIGWAccessLogging.ts +++ b/src/rules/apigw/APIGWAccessLogging.ts @@ -8,7 +8,7 @@ import { CfnStage } from 'aws-cdk-lib/aws-apigateway'; import { CfnStage as CfnV2Stage } from 'aws-cdk-lib/aws-apigatewayv2'; import { NagRuleCompliance } from '../../nag-rules'; /** - * APIs have access logging enabled + * API Gateway stages have access logging enabled * @param node the CfnResource to check */ export default Object.defineProperty( diff --git a/src/rules/apigw/APIGWDefaultThrottling.ts b/src/rules/apigw/APIGWDefaultThrottling.ts new file mode 100644 index 0000000000..2a775bd36e --- /dev/null +++ b/src/rules/apigw/APIGWDefaultThrottling.ts @@ -0,0 +1,46 @@ +import { CfnResource, Stack } from 'aws-cdk-lib'; +import { CfnStage } from 'aws-cdk-lib/aws-apigateway'; +import { CfnStage as CfnHttpStage } from 'aws-cdk-lib/aws-apigatewayv2'; +import { NagRuleCompliance } from '../../nag-rules'; + +/** + * API Gateway stages are not using default throttling settings + * @param node The CfnStage or CfnHttpStage to check + */ +export default Object.defineProperty( + (node: CfnResource): NagRuleCompliance => { + if (node instanceof CfnStage) { + // Check REST API + const methodSettings = Stack.of(node).resolve(node.methodSettings); + if ( + Array.isArray(methodSettings) && + methodSettings.some( + (setting) => + setting.throttlingBurstLimit !== undefined && + setting.throttlingRateLimit !== undefined && + setting.httpMethod === '*' && + setting.resourcePath === '/*' + ) + ) { + return NagRuleCompliance.COMPLIANT; + } + return NagRuleCompliance.NON_COMPLIANT; + } else if (node instanceof CfnHttpStage) { + // Check HTTP API + const defaultRouteSettings = Stack.of(node).resolve( + node.defaultRouteSettings + ); + if ( + defaultRouteSettings && + defaultRouteSettings.throttlingBurstLimit !== undefined && + defaultRouteSettings.throttlingRateLimit !== undefined + ) { + return NagRuleCompliance.COMPLIANT; + } + return NagRuleCompliance.NON_COMPLIANT; + } + return NagRuleCompliance.NOT_APPLICABLE; + }, + 'name', + { value: 'APIGWDefaultThrottling' } +); diff --git a/src/rules/apigw/APIGWStructuredLogging.ts b/src/rules/apigw/APIGWStructuredLogging.ts new file mode 100644 index 0000000000..93ebe31c01 --- /dev/null +++ b/src/rules/apigw/APIGWStructuredLogging.ts @@ -0,0 +1,52 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { parse } from 'path'; +import { CfnResource, Stack } from 'aws-cdk-lib'; +import { CfnDeployment, CfnStage } from 'aws-cdk-lib/aws-apigateway'; +import { CfnStage as CfnStageV2 } from 'aws-cdk-lib/aws-apigatewayv2'; +import { CfnApi, CfnHttpApi } from 'aws-cdk-lib/aws-sam'; +import { NagRuleCompliance } from '../../nag-rules'; + +const isJSON = (str: string) => { + try { + JSON.parse(str); + } catch (e) { + return false; + } + return true; +}; + +/** + * API Gateway logs are configured in JSON format. + * @param node the CfnResource to check + */ +export default Object.defineProperty( + (node: CfnResource): NagRuleCompliance => { + if ( + node instanceof CfnApi || + node instanceof CfnHttpApi || + node instanceof CfnStage + ) { + const accessLogSetting = Stack.of(node).resolve(node.accessLogSetting); + if (!accessLogSetting) return NagRuleCompliance.NOT_APPLICABLE; + if (isJSON(accessLogSetting.format)) return NagRuleCompliance.COMPLIANT; + return NagRuleCompliance.NON_COMPLIANT; + } else if (node instanceof CfnDeployment) { + const stageDescription = Stack.of(node).resolve(node.stageDescription); + const accessLogSetting = stageDescription.accessLogSetting; + if (!accessLogSetting) return NagRuleCompliance.NOT_APPLICABLE; + if (isJSON(accessLogSetting.format)) return NagRuleCompliance.COMPLIANT; + return NagRuleCompliance.NON_COMPLIANT; + } else if (node instanceof CfnStageV2) { + const accessLogSetting = Stack.of(node).resolve(node.accessLogSettings); + if (!accessLogSetting) return NagRuleCompliance.NOT_APPLICABLE; + if (isJSON(accessLogSetting.format)) return NagRuleCompliance.COMPLIANT; + return NagRuleCompliance.NON_COMPLIANT; + } + return NagRuleCompliance.NOT_APPLICABLE; + }, + 'name', + { value: parse(__filename).name } +); diff --git a/src/rules/apigw/index.ts b/src/rules/apigw/index.ts index d96825150e..add671d09f 100644 --- a/src/rules/apigw/index.ts +++ b/src/rules/apigw/index.ts @@ -6,7 +6,10 @@ export { default as APIGWAccessLogging } from './APIGWAccessLogging'; export { default as APIGWAssociatedWithWAF } from './APIGWAssociatedWithWAF'; export { default as APIGWAuthorization } from './APIGWAuthorization'; export { default as APIGWCacheEnabledAndEncrypted } from './APIGWCacheEnabledAndEncrypted'; +export { default as APIGWDefaultThrottling } from './APIGWDefaultThrottling'; export { default as APIGWExecutionLoggingEnabled } from './APIGWExecutionLoggingEnabled'; export { default as APIGWRequestValidation } from './APIGWRequestValidation'; export { default as APIGWSSLEnabled } from './APIGWSSLEnabled'; +export { default as APIGWStructuredLogging } from './APIGWStructuredLogging'; export { default as APIGWXrayEnabled } from './APIGWXrayEnabled'; +export { default as APIGWStructuredLogging } from './APIGWStructuredLogging' \ No newline at end of file diff --git a/src/rules/appsync/AppSyncTracing.ts b/src/rules/appsync/AppSyncTracing.ts new file mode 100644 index 0000000000..1d2188c7bb --- /dev/null +++ b/src/rules/appsync/AppSyncTracing.ts @@ -0,0 +1,25 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { parse } from 'path'; +import { CfnResource, Stack } from 'aws-cdk-lib'; +import { CfnGraphQLApi } from 'aws-cdk-lib/aws-appsync'; +import { NagRuleCompliance } from '../../nag-rules'; + +/** + * AppSync APIs have tracing enabled + * @param node the CfnResource to check + */ +export default Object.defineProperty( + (node: CfnResource): NagRuleCompliance => { + if (node instanceof CfnGraphQLApi) { + const isXrayEnabled = Stack.of(node).resolve(node.xrayEnabled); + if (isXrayEnabled) return NagRuleCompliance.COMPLIANT; + return NagRuleCompliance.NON_COMPLIANT; + } + return NagRuleCompliance.NOT_APPLICABLE; + }, + 'name', + { value: parse(__filename).name } +); diff --git a/src/rules/appsync/index.ts b/src/rules/appsync/index.ts index 44999551db..f8501051ab 100644 --- a/src/rules/appsync/index.ts +++ b/src/rules/appsync/index.ts @@ -3,3 +3,4 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ export { default as AppSyncGraphQLRequestLogging } from './AppSyncGraphQLRequestLogging'; +export { default as AppSyncTracing } from './AppSyncTracing'; diff --git a/src/rules/eventbridge/EventBusDLQ.ts b/src/rules/eventbridge/EventBusDLQ.ts new file mode 100644 index 0000000000..6c1ed80b21 --- /dev/null +++ b/src/rules/eventbridge/EventBusDLQ.ts @@ -0,0 +1,28 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { parse } from 'path'; +import { CfnResource, Stack } from 'aws-cdk-lib'; +import { CfnRule } from 'aws-cdk-lib/aws-events'; +import { NagRuleCompliance } from '../../nag-rules'; + +/** + * EventBridge targets have a Dead Letter Queue configured. + * @param node the CfnResource to check + */ +export default Object.defineProperty( + (node: CfnResource): NagRuleCompliance => { + if (node instanceof CfnRule) { + const targets: CfnRule.TargetProperty[] = Stack.of(node).resolve( + node.targets + ); + if (targets.every((target) => target.deadLetterConfig !== undefined)) + return NagRuleCompliance.COMPLIANT; + return NagRuleCompliance.NON_COMPLIANT; + } + return NagRuleCompliance.NOT_APPLICABLE; + }, + 'name', + { value: parse(__filename).name } +); diff --git a/src/rules/eventbridge/index.ts b/src/rules/eventbridge/index.ts index f968a0a84c..c69fa15a2b 100644 --- a/src/rules/eventbridge/index.ts +++ b/src/rules/eventbridge/index.ts @@ -3,3 +3,4 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ export { default as EventBusOpenAccess } from './EventBusOpenAccess'; +export { default as EventBusDLQ } from './EventBusDLQ'; diff --git a/src/rules/lambda/LambdaAsyncFailureDestination.ts b/src/rules/lambda/LambdaAsyncFailureDestination.ts new file mode 100644 index 0000000000..7b25ef96ca --- /dev/null +++ b/src/rules/lambda/LambdaAsyncFailureDestination.ts @@ -0,0 +1,26 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { parse } from 'path'; +import { CfnResource, Stack } from 'aws-cdk-lib'; +import { CfnEventInvokeConfig } from 'aws-cdk-lib/aws-lambda'; +import { NagRuleCompliance } from '../../nag-rules'; + +/** + * Lambda functions with asynchronous invocations should have a failure destination + * @param node the CfnResource to check + */ +export default Object.defineProperty( + (node: CfnResource): NagRuleCompliance => { + if (node instanceof CfnEventInvokeConfig) { + const destinationConfig = Stack.of(node).resolve(node.destinationConfig); + if (destinationConfig?.onFailure?.destination) + return NagRuleCompliance.COMPLIANT; + return NagRuleCompliance.NON_COMPLIANT; + } + return NagRuleCompliance.NOT_APPLICABLE; + }, + 'name', + { value: parse(__filename).name } +); diff --git a/src/rules/lambda/LambdaDLQ.ts b/src/rules/lambda/LambdaDLQ.ts index 04dfe8c70b..cb57699b5d 100644 --- a/src/rules/lambda/LambdaDLQ.ts +++ b/src/rules/lambda/LambdaDLQ.ts @@ -17,7 +17,7 @@ export default Object.defineProperty( const deadLetterConfig = Stack.of(node).resolve(node.deadLetterConfig); if ( deadLetterConfig == undefined || - deadLetterConfig.targetArn == undefined + deadLetterConfig.targetArn == undefined ) { return NagRuleCompliance.NON_COMPLIANT; } diff --git a/src/rules/lambda/LambdaDefaultMemorySize.ts b/src/rules/lambda/LambdaDefaultMemorySize.ts new file mode 100644 index 0000000000..7f4d4067bb --- /dev/null +++ b/src/rules/lambda/LambdaDefaultMemorySize.ts @@ -0,0 +1,26 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { parse } from 'path'; +import { CfnResource, Stack } from 'aws-cdk-lib'; +import { CfnFunction } from 'aws-cdk-lib/aws-lambda'; +import { NagRuleCompliance } from '../../nag-rules'; + +/** + * Lambda allocates CPU power in proportion to the amount of memory configured. By default, your functions have 128 MB of memory allocated. You can increase that value up to 10 GB. With more CPU resources, your Lambda function's duration might decrease. Lambda functions should have an explicit memory value configured rather than using the default value. + * You can use tools such as AWS Lambda Power Tuning to test your function at different memory settings to find the one that matches your cost and performance requirements the best. + * @param node the CfnResource to check + */ +export default Object.defineProperty( + (node: CfnResource): NagRuleCompliance => { + if (node instanceof CfnFunction) { + const memorySize = Stack.of(node).resolve(node.memorySize); + if (memorySize) return NagRuleCompliance.COMPLIANT; + return NagRuleCompliance.NON_COMPLIANT; + } + return NagRuleCompliance.NOT_APPLICABLE; + }, + 'name', + { value: parse(__filename).name } +); diff --git a/src/rules/lambda/LambdaDefaultTimeout.ts b/src/rules/lambda/LambdaDefaultTimeout.ts new file mode 100644 index 0000000000..bcb06f9fcc --- /dev/null +++ b/src/rules/lambda/LambdaDefaultTimeout.ts @@ -0,0 +1,25 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { parse } from 'path'; +import { CfnResource, Stack } from 'aws-cdk-lib'; +import { CfnFunction } from 'aws-cdk-lib/aws-lambda'; +import { NagRuleCompliance } from '../../nag-rules'; + +/** + * Lambda functions must have an explicitly defined timeout value. + * @param node the CfnResource to check + */ +export default Object.defineProperty( + (node: CfnResource): NagRuleCompliance => { + if (node instanceof CfnFunction) { + const timeout = Stack.of(node).resolve(node.timeout); + if (timeout) return NagRuleCompliance.COMPLIANT; + return NagRuleCompliance.NON_COMPLIANT; + } + return NagRuleCompliance.NOT_APPLICABLE; + }, + 'name', + { value: parse(__filename).name } +); diff --git a/src/rules/lambda/LambdaEventSourceMappingDestination.ts b/src/rules/lambda/LambdaEventSourceMappingDestination.ts new file mode 100644 index 0000000000..3406d759db --- /dev/null +++ b/src/rules/lambda/LambdaEventSourceMappingDestination.ts @@ -0,0 +1,24 @@ +import { CfnResource, Stack } from 'aws-cdk-lib'; +import { CfnEventSourceMapping } from 'aws-cdk-lib/aws-lambda'; +import { NagRuleCompliance } from '../../nag-rules'; + +/** + * Lambda Event Source Mappings must have a destination configured for failed invocations. + * + * @param node - The CfnResource to check + */ +export default function LambdaEventSourceMappingDestination( + node: CfnResource +): NagRuleCompliance { + if (node instanceof CfnEventSourceMapping) { + const destinationConfig = Stack.of(node).resolve(node.destinationConfig); + if ( + destinationConfig?.onFailure && + destinationConfig?.onFailure?.destination + ) + return NagRuleCompliance.COMPLIANT; + + return NagRuleCompliance.NON_COMPLIANT; + } + return NagRuleCompliance.NOT_APPLICABLE; +} diff --git a/src/rules/lambda/LambdaLogLevel.ts b/src/rules/lambda/LambdaLogLevel.ts new file mode 100644 index 0000000000..0a74c65111 --- /dev/null +++ b/src/rules/lambda/LambdaLogLevel.ts @@ -0,0 +1,26 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { parse } from 'path'; +import { CfnResource, Stack } from 'aws-cdk-lib'; +import { CfnFunction } from 'aws-cdk-lib/aws-lambda'; +import { NagRuleCompliance } from '../../nag-rules'; + +/** + * By default, CloudWatch log groups created by Lambda functions have an unlimited retention time. For cost optimization purposes, you should explicitly define a LogGroup which allows for the CloudWatchLogGroupRetentionPeriod rule to detect unspecified log retention periods. + * @param node the CfnResource to check + */ +export default Object.defineProperty( + (node: CfnResource): NagRuleCompliance => { + if (node instanceof CfnFunction) { + const loggingConfig = Stack.of(node).resolve(node.loggingConfig); + if (loggingConfig && loggingConfig.logGroup) + return NagRuleCompliance.COMPLIANT; + return NagRuleCompliance.NON_COMPLIANT; + } + return NagRuleCompliance.NOT_APPLICABLE; + }, + 'name', + { value: parse(__filename).name } +); diff --git a/src/rules/lambda/LambdaLogging.ts b/src/rules/lambda/LambdaLogging.ts new file mode 100644 index 0000000000..674a0fc4bc --- /dev/null +++ b/src/rules/lambda/LambdaLogging.ts @@ -0,0 +1,24 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { parse } from 'path'; +import { CfnResource, Stack } from 'aws-cdk-lib'; +import { CfnFunction } from 'aws-cdk-lib/aws-lambda'; +import { NagRuleCompliance } from '../../nag-rules'; + +/** + * Ensure that Lambda functions have a corresponding Log Group + * @param node the CfnResource to check + */ +export default Object.defineProperty( + (node: CfnResource): NagRuleCompliance => { + if (node instanceof CfnFunction) { + const loggingConfig = Stack.of(node).resolve(node.loggingConfig); + if (loggingConfig) return NagRuleCompliance.COMPLIANT; + return NagRuleCompliance.NON_COMPLIANT; + } + return NagRuleCompliance.NOT_APPLICABLE; + }, + 'name', { value: parse(__filename).name } +); \ No newline at end of file diff --git a/src/rules/lambda/LambdaTracing.ts b/src/rules/lambda/LambdaTracing.ts new file mode 100644 index 0000000000..4db789b578 --- /dev/null +++ b/src/rules/lambda/LambdaTracing.ts @@ -0,0 +1,25 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { CfnResource, Stack } from 'aws-cdk-lib'; +import { CfnFunction } from 'aws-cdk-lib/aws-lambda'; +import { parse } from 'path'; +import { NagRuleCompliance } from '../../nag-rules'; + +/** + * Ensure Lambda functions have tracing enabled + * @param node the CfnResource to check + */ +export default Object.defineProperty( + (node: CfnResource): NagRuleCompliance => { + if (node instanceof CfnFunction) { + const tracingConfig = Stack.of(node).resolve(node.tracingConfig) as CfnFunction.TracingConfigProperty | undefined; + if (tracingConfig?.mode === 'Active') return NagRuleCompliance.COMPLIANT + return NagRuleCompliance.NON_COMPLIANT; + } + return NagRuleCompliance.NOT_APPLICABLE; + }, + 'name', + { value: parse(__filename).name } +); diff --git a/src/rules/lambda/index.ts b/src/rules/lambda/index.ts index 8d96afe93e..6535aa70ea 100644 --- a/src/rules/lambda/index.ts +++ b/src/rules/lambda/index.ts @@ -3,9 +3,14 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ +export { default as LambdaAsyncFailureDestination } from './LambdaAsyncFailureDestination'; export { default as LambdaConcurrency } from './LambdaConcurrency'; +export { default as LambdaDefaultMemorySize } from './LambdaDefaultMemorySize'; +export { default as LambdaDefaultTimeout } from './LambdaDefaultTimeout'; export { default as LambdaDLQ } from './LambdaDLQ'; +export { default as LambdaEventSourceMappingDestination } from './LambdaEventSourceMappingDestination'; export { default as LambdaFunctionPublicAccessProhibited } from './LambdaFunctionPublicAccessProhibited'; export { default as LambdaFunctionUrlAuth } from './LambdaFunctionUrlAuth'; export { default as LambdaInsideVPC } from './LambdaInsideVPC'; export { default as LambdaLatestVersion } from './LambdaLatestVersion'; +export { default as LambdaTracing } from './LambdaTracing'; diff --git a/src/rules/sns/SNSDeadLetterQueue.ts b/src/rules/sns/SNSDeadLetterQueue.ts new file mode 100644 index 0000000000..e88d3bddb9 --- /dev/null +++ b/src/rules/sns/SNSDeadLetterQueue.ts @@ -0,0 +1,25 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { parse } from 'path'; +import { CfnResource } from 'aws-cdk-lib'; +import { NagRuleCompliance } from '../../nag-rules'; +import { CfnSubscription } from 'aws-cdk-lib/aws-sns'; + + +/** + * Ensure that API Gateway REST and HTTP APIs are using JSON structured logs + * @param node the CfnResource to check + */ +export default Object.defineProperty( + (node: CfnResource): NagRuleCompliance => { + if (node instanceof CfnSubscription) { + const redrivePolicy = node.redrivePolicy; + if (redrivePolicy) return NagRuleCompliance.COMPLIANT; + return NagRuleCompliance.NON_COMPLIANT; + } + return NagRuleCompliance.NOT_APPLICABLE; + }, + 'name', { value: parse(__filename).name } +); \ No newline at end of file diff --git a/src/rules/sns/SNSRedrivePolicy.ts b/src/rules/sns/SNSRedrivePolicy.ts new file mode 100644 index 0000000000..75886eb8ab --- /dev/null +++ b/src/rules/sns/SNSRedrivePolicy.ts @@ -0,0 +1,22 @@ +import { parse } from 'path'; +import { CfnResource } from 'aws-cdk-lib'; +import { CfnSubscription } from 'aws-cdk-lib/aws-sns'; +import { NagRuleCompliance } from '../../nag-rules'; + +/** + * SNS subscriptions have a redrive policy configured. + * + * @see https://docs.aws.amazon.com/sns/latest/dg/sns-dead-letter-queues.html + */ +export default Object.defineProperty( + (node: CfnResource): NagRuleCompliance => { + if (node instanceof CfnSubscription) { + if (node.redrivePolicy === undefined) + return NagRuleCompliance.NON_COMPLIANT; + return NagRuleCompliance.COMPLIANT; + } + return NagRuleCompliance.NOT_APPLICABLE; + }, + 'name', + { value: parse(__filename).name } +); diff --git a/src/rules/sns/index.ts b/src/rules/sns/index.ts index 4d45acee84..ba987bde58 100644 --- a/src/rules/sns/index.ts +++ b/src/rules/sns/index.ts @@ -3,4 +3,7 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ export { default as SNSEncryptedKMS } from './SNSEncryptedKMS'; +export { default as SNSRedrivePolicy } from './SNSRedrivePolicy'; export { default as SNSTopicSSLPublishOnly } from './SNSTopicSSLPublishOnly'; +export { default as SNSDeadLetterQueue } from './SNSDeadLetterQueue' + diff --git a/src/rules/sqs/SQSRedrivePolicy.ts b/src/rules/sqs/SQSRedrivePolicy.ts new file mode 100644 index 0000000000..a9f52fe67a --- /dev/null +++ b/src/rules/sqs/SQSRedrivePolicy.ts @@ -0,0 +1,17 @@ +import { CfnResource, Stack } from 'aws-cdk-lib'; +import { CfnQueue } from 'aws-cdk-lib/aws-sqs'; +import { NagRuleCompliance } from '../../nag-rules'; + +/** + * SQS queues have a redrive policy configured + * + * @param node - the CfnResource to check + */ +export default function SQSRedrivePolicy(node: CfnResource): NagRuleCompliance { + if (node instanceof CfnQueue) { + const redrivePolicy = Stack.of(node).resolve(node.redrivePolicy); + if (redrivePolicy !== undefined) return NagRuleCompliance.COMPLIANT; + return NagRuleCompliance.NON_COMPLIANT; + } + return NagRuleCompliance.NOT_APPLICABLE; +} diff --git a/src/rules/sqs/index.ts b/src/rules/sqs/index.ts index 5e6f9a2a9e..fb561e2d30 100644 --- a/src/rules/sqs/index.ts +++ b/src/rules/sqs/index.ts @@ -5,3 +5,4 @@ SPDX-License-Identifier: Apache-2.0 export { default as SQSQueueDLQ } from './SQSQueueDLQ'; export { default as SQSQueueSSE } from './SQSQueueSSE'; export { default as SQSQueueSSLRequestsOnly } from './SQSQueueSSLRequestsOnly'; +export { default as SQSRedrivePolicy } from './SQSRedrivePolicy'; diff --git a/src/rules/stepfunctions/StepFunctionStateMachineXray.ts b/src/rules/stepfunctions/StepFunctionStateMachineXray.ts index 6459cea39d..d1cb79b5a6 100644 --- a/src/rules/stepfunctions/StepFunctionStateMachineXray.ts +++ b/src/rules/stepfunctions/StepFunctionStateMachineXray.ts @@ -8,7 +8,7 @@ import { CfnStateMachine } from 'aws-cdk-lib/aws-stepfunctions'; import { NagRuleCompliance, NagRules } from '../../nag-rules'; /** - * Step Function have X-Ray tracing enabled + * StepFunctions have X-Ray tracing configured. * @param node the CfnResource to check */ export default Object.defineProperty( diff --git a/test/rules/APIGW.test.ts b/test/rules/APIGW.test.ts index d45ef110f5..1da5dfba12 100644 --- a/test/rules/APIGW.test.ts +++ b/test/rules/APIGW.test.ts @@ -5,6 +5,7 @@ SPDX-License-Identifier: Apache-2.0 import { AuthorizationType, CfnClientCertificate, + CfnDeployment, CfnRequestValidator, CfnRestApi, CfnStage, @@ -12,6 +13,7 @@ import { RestApi, } from 'aws-cdk-lib/aws-apigateway'; import { CfnRoute, CfnStage as CfnV2Stage } from 'aws-cdk-lib/aws-apigatewayv2'; +import { CfnApi, CfnHttpApi } from 'aws-cdk-lib/aws-sam'; import { CfnWebACLAssociation } from 'aws-cdk-lib/aws-wafv2'; import { Aspects, Stack } from 'aws-cdk-lib/core'; import { TestPack, TestType, validateStack } from './utils'; @@ -20,9 +22,11 @@ import { APIGWAssociatedWithWAF, APIGWAuthorization, APIGWCacheEnabledAndEncrypted, + APIGWDefaultThrottling, APIGWExecutionLoggingEnabled, APIGWRequestValidation, APIGWSSLEnabled, + APIGWStructuredLogging, APIGWXrayEnabled, } from '../../src/rules/apigw'; @@ -35,6 +39,8 @@ const testPack = new TestPack([ APIGWRequestValidation, APIGWSSLEnabled, APIGWXrayEnabled, + APIGWStructuredLogging, + APIGWDefaultThrottling, ]); let stack: Stack; @@ -318,4 +324,189 @@ describe('Amazon API Gateway', () => { validateStack(stack, ruleId, TestType.COMPLIANCE); }); }); + + describe('APIGWStructuredLogging: API Gateway stages use JSON-formatted structured logging', () => { + const ruleId = 'APIGWStructuredLogging'; + + test('Noncompliance 1: Non-JSON format (CfnStage)', () => { + new CfnStage(stack, 'rRestApiStageNonJsonFormat', { + restApiId: 'foo', + stageName: 'prod', + accessLogSetting: { + destinationArn: + 'arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod', + format: + '$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] "$context.httpMethod $context.resourcePath $context.protocol" $context.status $context.responseLength $context.requestId', + }, + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance 1: JSON-formatted log (CfnStage)', () => { + new CfnStage(stack, 'rRestApiStageJsonFormat', { + restApiId: 'foo', + stageName: 'prod', + accessLogSetting: { + destinationArn: + 'arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod', + format: + '{"requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength"}', + }, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Compliance 2: HTTP API with JSON-formatted log (CfnStageV2)', () => { + new CfnV2Stage(stack, 'rHttpApiStageJsonFormat', { + apiId: 'bar', + stageName: 'prod', + accessLogSettings: { + destinationArn: + 'arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod', + format: + '{"requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","routeKey":"$context.routeKey", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength"}', + }, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Noncompliance 2: No access log settings (CfnDeployment)', () => { + new CfnDeployment(stack, 'rRestApiDeploymentNoLogs', { + restApiId: 'foo', + stageDescription: { + accessLogSetting: { + destinationArn: + 'arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod', + }, + }, + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance 3: JSON-formatted log (CfnDeployment)', () => { + new CfnDeployment(stack, 'rRestApiDeploymentJsonFormat', { + restApiId: 'foo', + stageDescription: { + accessLogSetting: { + destinationArn: + 'arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod', + format: + '{"requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength"}', + }, + }, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Noncompliance 3: No access log settings (CfnApi)', () => { + new CfnApi(stack, 'rSamApiNoLogs', { + stageName: 'MyApi', + accessLogSetting: { + destinationArn: + 'arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod', + }, + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance 4: JSON-formatted log (CfnApi)', () => { + new CfnApi(stack, 'rSamApiJsonFormat', { + stageName: 'MyApi', + accessLogSetting: { + destinationArn: + 'arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod', + format: + '{"requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength"}', + }, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Noncompliance 4: No access log settings (CfnHttpApi)', () => { + new CfnHttpApi(stack, 'rSamHttpApiNoLogs', { + stageName: 'MyApi', + accessLogSetting: { + destinationArn: + 'arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod', + }, + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance 5: JSON-formatted log (CfnHttpApi)', () => { + new CfnHttpApi(stack, 'rSamHttpApiJsonFormat', { + stageName: 'MyApi', + accessLogSetting: { + destinationArn: + 'arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod', + format: + '{"requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","routeKey":"$context.routeKey", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength"}', + }, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + }); + + describe('APIGWDefaultThrottling: API Gateway REST and HTTP APIs have default throttling enabled', () => { + const ruleId = 'APIGWDefaultThrottling'; + + test('Noncompliance 1: REST API without throttling', () => { + new CfnStage(stack, 'rRestApiStageNoThrottling', { + restApiId: 'foo', + stageName: 'prod', + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Noncompliance 2: HTTP API without throttling', () => { + new CfnV2Stage(stack, 'rHttpApiStageNoThrottling', { + apiId: 'bar', + stageName: 'prod', + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Noncompliance 3: REST API with incomplete throttling', () => { + new CfnStage(stack, 'rRestApiStageIncompleteThrottling', { + restApiId: 'foo', + stageName: 'prod', + methodSettings: [ + { + httpMethod: '*', + resourcePath: '/*', + throttlingRateLimit: 100, + }, + ], + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance 1: REST API with complete throttling', () => { + new CfnStage(stack, 'rRestApiStageCompliantThrottling', { + restApiId: 'foo', + stageName: 'prod', + methodSettings: [ + { + httpMethod: '*', + resourcePath: '/*', + throttlingRateLimit: 100, + throttlingBurstLimit: 50, + }, + ], + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Compliance 2: HTTP API with throttling', () => { + new CfnV2Stage(stack, 'rHttpApiStageCompliantThrottling', { + apiId: 'bar', + stageName: 'prod', + defaultRouteSettings: { + throttlingRateLimit: 100, + throttlingBurstLimit: 50, + }, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + }); }); diff --git a/test/rules/AppSync.test.ts b/test/rules/AppSync.test.ts index f91dfa2188..e0057977c9 100644 --- a/test/rules/AppSync.test.ts +++ b/test/rules/AppSync.test.ts @@ -4,10 +4,13 @@ SPDX-License-Identifier: Apache-2.0 */ import { CfnGraphQLApi } from 'aws-cdk-lib/aws-appsync'; import { Aspects, Stack } from 'aws-cdk-lib/core'; -import { validateStack, TestType, TestPack } from './utils'; -import { AppSyncGraphQLRequestLogging } from '../../src/rules/appsync'; +import { TestPack, TestType, validateStack } from './utils'; +import { + AppSyncGraphQLRequestLogging, + AppSyncTracing, +} from '../../src/rules/appsync'; -const testPack = new TestPack([AppSyncGraphQLRequestLogging]); +const testPack = new TestPack([AppSyncGraphQLRequestLogging, AppSyncTracing]); let stack: Stack; beforeEach(() => { @@ -42,4 +45,24 @@ describe('AWS AppSync', () => { validateStack(stack, ruleId, TestType.COMPLIANCE); }); }); + + describe('AppSyncTracing: GraphQL APIs have X-Ray tracing enabled', () => { + const ruleId = 'AppSyncTracing'; + test('Noncompliance 1', () => { + new CfnGraphQLApi(stack, 'rGraphqlApi', { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + name: 'foo', + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance - L1 Construct', () => { + new CfnGraphQLApi(stack, 'rGraphqlApi', { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + name: 'foo', + xrayEnabled: true, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + }); }); diff --git a/test/rules/EventBridge.test.ts b/test/rules/EventBridge.test.ts index 995af739f5..73b9854b5c 100644 --- a/test/rules/EventBridge.test.ts +++ b/test/rules/EventBridge.test.ts @@ -3,11 +3,15 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ import { Aspects, Stack } from 'aws-cdk-lib'; -import { CfnEventBusPolicy } from 'aws-cdk-lib/aws-events'; -import { validateStack, TestType, TestPack } from './utils'; -import { EventBusOpenAccess } from '../../src/rules/eventbridge'; +import * as events from 'aws-cdk-lib/aws-events'; +import { CfnEventBusPolicy, CfnRule } from 'aws-cdk-lib/aws-events'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as sqs from 'aws-cdk-lib/aws-sqs'; +import { TestPack, TestType, validateStack } from './utils'; +import { EventBusDLQ, EventBusOpenAccess } from '../../src/rules/eventbridge'; -const testPack = new TestPack([EventBusOpenAccess]); +const testPack = new TestPack([EventBusOpenAccess, EventBusDLQ]); let stack: Stack; beforeEach(() => { @@ -15,6 +19,202 @@ beforeEach(() => { Aspects.of(stack).add(testPack); }); +describe('EventBusDLQ: EventBridge rules have a Dead Letter Queue configured.', () => { + const ruleId = 'EventBusDLQ'; + + test('Noncompliance: Rule without DLQ', () => { + new CfnRule(stack, 'RuleWithoutDLQ', { + eventPattern: { + source: ['aws.ec2'], + }, + targets: [ + { + arn: 'arn:aws:lambda:us-east-1:111122223333:function:MyFunction', + id: 'Target1', + }, + ], + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance: Rule with DLQ', () => { + new CfnRule(stack, 'RuleWithDLQ', { + eventPattern: { + source: ['aws.ec2'], + }, + targets: [ + { + arn: 'arn:aws:lambda:us-east-1:111122223333:function:MyFunction', + id: 'Target1', + deadLetterConfig: { + arn: 'arn:aws:sqs:us-east-1:111122223333:MyDLQ', + }, + }, + ], + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Compliance: Rule with multiple targets, all having DLQs', () => { + new CfnRule(stack, 'RuleWithMultipleTargetsAllDLQ', { + eventPattern: { + source: ['aws.ec2'], + }, + targets: [ + { + arn: 'arn:aws:lambda:us-east-1:111122223333:function:Function1', + id: 'Target1', + deadLetterConfig: { + arn: 'arn:aws:sqs:us-east-1:111122223333:DLQ1', + }, + }, + { + arn: 'arn:aws:lambda:us-east-1:111122223333:function:Function2', + id: 'Target2', + deadLetterConfig: { + arn: 'arn:aws:sqs:us-east-1:111122223333:DLQ2', + }, + }, + ], + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Noncompliance: Rule with multiple targets, one missing DLQ', () => { + new CfnRule(stack, 'RuleWithMultipleTargetsOneMissingDLQ', { + eventPattern: { + source: ['aws.ec2'], + }, + targets: [ + { + arn: 'arn:aws:lambda:us-east-1:111122223333:function:Function1', + id: 'Target1', + deadLetterConfig: { + arn: 'arn:aws:sqs:us-east-1:111122223333:DLQ1', + }, + }, + { + arn: 'arn:aws:lambda:us-east-1:111122223333:function:Function2', + id: 'Target2', + }, + ], + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + describe('L2 Construct Tests', () => { + test('Noncompliance: L2 Rule without DLQ', () => { + const rule = new events.Rule(stack, 'L2RuleWithoutDLQ', { + eventPattern: { + source: ['aws.ec2'], + }, + }); + rule.addTarget( + new targets.LambdaFunction( + new lambda.Function(stack, 'MyLambda', { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'index.handler', + code: lambda.Code.fromInline('exports.handler = async () => {};'), + }) + ) + ); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance: L2 Rule with DLQ', () => { + const dlq = new sqs.Queue(stack, 'MyDLQ'); + const rule = new events.Rule(stack, 'L2RuleWithDLQ', { + eventPattern: { + source: ['aws.ec2'], + }, + }); + rule.addTarget( + new targets.LambdaFunction( + new lambda.Function(stack, 'MyLambda', { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'index.handler', + code: lambda.Code.fromInline('exports.handler = async () => {};'), + }), + { + deadLetterQueue: dlq, + } + ) + ); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Compliance: L2 Rule with multiple targets, all having DLQs', () => { + const dlq1 = new sqs.Queue(stack, 'MyDLQ1'); + const dlq2 = new sqs.Queue(stack, 'MyDLQ2'); + const rule = new events.Rule(stack, 'L2RuleWithMultipleTargetsAllDLQ', { + eventPattern: { + source: ['aws.ec2'], + }, + }); + rule.addTarget( + new targets.LambdaFunction( + new lambda.Function(stack, 'MyLambda1', { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'index.handler', + code: lambda.Code.fromInline('exports.handler = async () => {};'), + }), + { + deadLetterQueue: dlq1, + } + ) + ); + rule.addTarget( + new targets.LambdaFunction( + new lambda.Function(stack, 'MyLambda2', { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'index.handler', + code: lambda.Code.fromInline('exports.handler = async () => {};'), + }), + { + deadLetterQueue: dlq2, + } + ) + ); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Noncompliance: L2 Rule with multiple targets, one missing DLQ', () => { + const dlq = new sqs.Queue(stack, 'MyDLQ'); + const rule = new events.Rule( + stack, + 'L2RuleWithMultipleTargetsOneMissingDLQ', + { + eventPattern: { + source: ['aws.ec2'], + }, + } + ); + rule.addTarget( + new targets.LambdaFunction( + new lambda.Function(stack, 'MyLambda1', { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'index.handler', + code: lambda.Code.fromInline('exports.handler = async () => {};'), + }), + { + deadLetterQueue: dlq, + } + ) + ); + rule.addTarget( + new targets.LambdaFunction( + new lambda.Function(stack, 'MyLambda2', { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'index.handler', + code: lambda.Code.fromInline('exports.handler = async () => {};'), + }) + ) + ); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + }); +}); + describe('Amazon EventBridge', () => { describe('EventBusOpenAccess: DMS replication instances are not public', () => { const ruleId = 'EventBusOpenAccess'; diff --git a/test/rules/Lambda.test.ts b/test/rules/Lambda.test.ts index 1ef1b8c79b..fd7f223fa4 100644 --- a/test/rules/Lambda.test.ts +++ b/test/rules/Lambda.test.ts @@ -2,27 +2,39 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -import { Aspects, Stack } from 'aws-cdk-lib'; +import { Aspects, Duration, Stack } from 'aws-cdk-lib'; import { Repository } from 'aws-cdk-lib/aws-ecr'; import { + CfnEventInvokeConfig, + CfnEventSourceMapping, CfnFunction, CfnPermission, CfnUrl, Code, DockerImageCode, DockerImageFunction, + EventSourceMapping, Function, FunctionUrlAuthType, Runtime, + Tracing, } from 'aws-cdk-lib/aws-lambda'; +import { SqsDestination } from 'aws-cdk-lib/aws-lambda-destinations'; +import { SqsDlq } from 'aws-cdk-lib/aws-lambda-event-sources'; +import { Queue } from 'aws-cdk-lib/aws-sqs'; import { TestPack, TestType, validateStack } from './utils'; import { + LambdaAsyncFailureDestination, LambdaConcurrency, + LambdaDefaultMemorySize, + LambdaDefaultTimeout, LambdaDLQ, + LambdaEventSourceMappingDestination, LambdaFunctionPublicAccessProhibited, LambdaFunctionUrlAuth, LambdaInsideVPC, LambdaLatestVersion, + LambdaTracing, } from '../../src/rules/lambda'; const testPack = new TestPack([ @@ -32,6 +44,11 @@ const testPack = new TestPack([ LambdaFunctionUrlAuth, LambdaInsideVPC, LambdaLatestVersion, + LambdaTracing, + LambdaDefaultMemorySize, + LambdaEventSourceMappingDestination, + LambdaDefaultTimeout, + LambdaAsyncFailureDestination, ]); let stack: Stack; @@ -351,4 +368,279 @@ describe('AWS Lambda', () => { validateStack(stack, ruleId, TestType.VALIDATION_FAILURE); }); }); + + describe('LambdaTracing: Lambda functions have X-Ray tracing enabled', () => { + const ruleId = 'LambdaTracing'; + + test('Noncompliance 1 - Tracing not configured', () => { + new CfnFunction(stack, 'rFunction', { + code: {}, + role: 'somerole', + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Noncompliance 2 - Tracing disabled', () => { + new CfnFunction(stack, 'rFunction', { + code: {}, + role: 'somerole', + tracingConfig: { + mode: 'PassThrough', + }, + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance - Tracing enabled', () => { + new CfnFunction(stack, 'rFunction', { + code: {}, + role: 'somerole', + tracingConfig: { + mode: 'Active', + }, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Compliance - L2 construct with tracing enabled', () => { + new Function(stack, 'rFunction', { + runtime: Runtime.NODEJS_20_X, + code: Code.fromInline('exports.handler = async () => {};'), + handler: 'index.handler', + tracing: Tracing.ACTIVE, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Noncompliance 3 - L2 construct with tracing disabled', () => { + new Function(stack, 'rFunctionDisabled', { + runtime: Runtime.NODEJS_20_X, + code: Code.fromInline('exports.handler = async () => {};'), + handler: 'index.handler', + tracing: Tracing.DISABLED, + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + }); + + describe('LambdaEventSourceMappingDestination: Lambda event source mappings have a failure destination configured', () => { + const ruleId = 'LambdaEventSourceMappingDestination'; + + test('Noncompliance 1 - No destinationConfig', () => { + new CfnEventSourceMapping(stack, 'rEventSourceMapping1', { + functionName: 'myFunction', + eventSourceArn: 'myEventSourceArn', + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Noncompliance 2 - onFailure without destination', () => { + new CfnEventSourceMapping(stack, 'rEventSourceMapping4', { + functionName: 'myFunction', + eventSourceArn: 'myEventSourceArn', + destinationConfig: { + onFailure: {}, + }, + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance - Proper failure destination configured', () => { + new CfnEventSourceMapping(stack, 'rEventSourceMapping5', { + functionName: 'myFunction', + eventSourceArn: 'myEventSourceArn', + destinationConfig: { + onFailure: { + destination: 'arn:aws:sqs:us-east-1:123456789012:myQueue', + }, + }, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Noncompliance 3 - L2 construct without onFailure', () => { + const lambdaFunction = new Function(stack, 'MyFunction1', { + runtime: Runtime.NODEJS_20_X, + handler: 'index.handler', + code: Code.fromInline('exports.handler = async () => {};'), + }); + + new EventSourceMapping(stack, 'MyEventSourceMapping1', { + target: lambdaFunction, + eventSourceArn: 'arn:aws:sqs:us-east-1:123456789012:myQueue', + }); + + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance - L2 construct with onFailure', () => { + const lambdaFunction = new Function(stack, 'MyFunction2', { + runtime: Runtime.NODEJS_20_X, + handler: 'index.handler', + code: Code.fromInline('exports.handler = async () => {};'), + }); + + const deadLetterQueue = new Queue(stack, 'DeadLetterQueue'); + + new EventSourceMapping(stack, 'MyEventSourceMapping2', { + target: lambdaFunction, + eventSourceArn: 'arn:aws:sqs:us-east-1:123456789012:myQueue', + onFailure: new SqsDlq(deadLetterQueue), + }); + + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + }); + + describe('LambdaDefaultMemorySize: Lambda functions should not use the default memory size', () => { + const ruleId = 'LambdaDefaultMemorySize'; + + test('Noncompliance 1 - Default memory size (128 MB)', () => { + new CfnFunction(stack, 'rFunction', { + code: {}, + role: 'somerole', + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Noncompliance 2 - L2 Construct set to default memory size', () => { + new Function(stack, 'rFunction', { + runtime: Runtime.NODEJS_20_X, + code: Code.fromInline('exports.handler = async () => {};'), + handler: 'index.handler', + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance 1 - L1 construct with non-default memory size', () => { + new CfnFunction(stack, 'rFunction', { + code: {}, + role: 'somerole', + memorySize: 128, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Compliance 2 - L2 construct with non-default memory size', () => { + new Function(stack, 'rFunction', { + runtime: Runtime.NODEJS_20_X, + code: Code.fromInline('exports.handler = async () => {};'), + handler: 'index.handler', + memorySize: 512, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + }); + + describe('LambdaDefaultTimeout: Lambda functions should not use the default timeout', () => { + const ruleId = 'LambdaDefaultTimeout'; + + test('Noncompliance 1 - Default timeout (3 seconds)', () => { + new CfnFunction(stack, 'rFunction', { + code: {}, + role: 'somerole', + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Noncompliance 2 - L2 construct explicitly using the default timeout', () => { + new Function(stack, 'rFunction', { + runtime: Runtime.NODEJS_20_X, + code: Code.fromInline('exports.handler = async () => {};'), + handler: 'index.handler', + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance 1 - L1 construct with non-default timeout', () => { + new CfnFunction(stack, 'rFunction', { + code: {}, + role: 'somerole', + timeout: 10, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Compliance 2 - L2 construct with non-default timeout', () => { + new Function(stack, 'rFunction', { + runtime: Runtime.NODEJS_20_X, + code: Code.fromInline('exports.handler = async () => {};'), + handler: 'index.handler', + timeout: Duration.seconds(30), + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + }); + + describe('LambdaAsyncFailureDestination: Lambda functions with async invocation should have a failure destination', () => { + const ruleId = 'LambdaAsyncFailureDestination'; + + test('Noncompliance 1 - Lambda function with async event invoke but no failure handler', () => { + const lambdaFunction = new CfnFunction(stack, 'rFunction', { + code: {}, + role: 'somerole', + }); + new CfnEventInvokeConfig(stack, 'rEventInvokeConfig', { + functionName: lambdaFunction.ref, + qualifier: '$LATEST', + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Noncompliance 2 - Lambda function with async event invoke but no failure handler', () => { + const lambdaFunction = new Function(stack, 'rFunction', { + runtime: Runtime.NODEJS_20_X, + code: Code.fromInline('exports.handler = async () => {};'), + handler: 'index.handler', + }); + const queue = new Queue(stack, 'DestinationQueue'); + lambdaFunction.configureAsyncInvoke({ + onSuccess: new SqsDestination(queue), + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Noncompliance 3 - L2 Lambda function with async invocation handler for successes but no failure handler', () => { + const lambdaFunction = new Function(stack, 'rFunction', { + runtime: Runtime.NODEJS_20_X, + code: Code.fromInline('exports.handler = async () => {};'), + handler: 'index.handler', + }); + const queue = new Queue(stack, 'DestinationQueue'); + lambdaFunction.configureAsyncInvoke({ + onSuccess: new SqsDestination(queue), + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance - Lambda function with proper async failure destination', () => { + const lambdaFunction = new CfnFunction(stack, 'rFunction', { + code: {}, + role: 'somerole', + }); + new CfnEventInvokeConfig(stack, 'rEventInvokeConfig', { + functionName: lambdaFunction.ref, + qualifier: '$LATEST', + destinationConfig: { + onFailure: { + destination: 'arn:aws:sqs:us-east-1:123456789012:myQueue', + }, + }, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Compliance - L2 construct with async failure destination', () => { + const lambdaFunction = new Function(stack, 'rFunction', { + runtime: Runtime.NODEJS_20_X, + code: Code.fromInline('exports.handler = async () => {};'), + handler: 'index.handler', + }); + const queue = new Queue(stack, 'DestinationQueue'); + lambdaFunction.configureAsyncInvoke({ + onFailure: new SqsDestination(queue), + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + }); }); diff --git a/test/rules/SNS.test.ts b/test/rules/SNS.test.ts index 2e6c13add3..131202fd35 100644 --- a/test/rules/SNS.test.ts +++ b/test/rules/SNS.test.ts @@ -3,19 +3,29 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ import { + AnyPrincipal, + Effect, PolicyDocument, PolicyStatement, - Effect, - AnyPrincipal, StarPrincipal, } from 'aws-cdk-lib/aws-iam'; import { Key } from 'aws-cdk-lib/aws-kms'; -import { CfnTopicPolicy, Topic } from 'aws-cdk-lib/aws-sns'; +import { CfnSubscription, CfnTopicPolicy, Topic } from 'aws-cdk-lib/aws-sns'; +import { SqsSubscription } from 'aws-cdk-lib/aws-sns-subscriptions'; +import { Queue } from 'aws-cdk-lib/aws-sqs'; import { Aspects, Stack } from 'aws-cdk-lib/core'; -import { validateStack, TestType, TestPack } from './utils'; -import { SNSEncryptedKMS, SNSTopicSSLPublishOnly } from '../../src/rules/sns'; +import { TestPack, TestType, validateStack } from './utils'; +import { + SNSEncryptedKMS, + SNSRedrivePolicy, + SNSTopicSSLPublishOnly, +} from '../../src/rules/sns'; -const testPack = new TestPack([SNSEncryptedKMS, SNSTopicSSLPublishOnly]); +const testPack = new TestPack([ + SNSEncryptedKMS, + SNSTopicSSLPublishOnly, + SNSRedrivePolicy, +]); let stack: Stack; beforeEach(() => { @@ -89,4 +99,48 @@ describe('Amazon Simple Notification Service (Amazon SNS)', () => { validateStack(stack, ruleId, TestType.COMPLIANCE); }); }); + + describe('SNSRedrivePolicy: SNS subscriptions have a redrive policy configured.', () => { + const ruleId = 'SNSRedrivePolicy'; + + test('Noncompliance: CfnSubscription without redrive policy', () => { + new CfnSubscription(stack, 'rSubscription', { + topicArn: 'arn:aws:sns:us-east-1:123456789012:MyTopic', + protocol: 'sqs', + endpoint: 'arn:aws:sqs:us-east-1:123456789012:MyQueue', + }); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Noncompliance: Subscription without redrive policy', () => { + const topic = new Topic(stack, 'rTopic'); + const queue = new Queue(stack, 'rQueue'); + topic.addSubscription(new SqsSubscription(queue)); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance: CfnSubscription with redrive policy', () => { + new CfnSubscription(stack, 'rSubscription', { + topicArn: 'arn:aws:sns:us-east-1:123456789012:MyTopic', + protocol: 'sqs', + endpoint: 'arn:aws:sqs:us-east-1:123456789012:MyQueue', + redrivePolicy: { + deadLetterTargetArn: 'arn:aws:sqs:us-east-1:123456789012:MyDLQ', + }, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Compliance: Subscription with redrive policy', () => { + const topic = new Topic(stack, 'rTopic'); + const queue = new Queue(stack, 'rQueue'); + const dlq = new Queue(stack, 'rDLQ'); + topic.addSubscription( + new SqsSubscription(queue, { + deadLetterQueue: dlq, + }) + ); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + }); }); diff --git a/test/rules/SQS.test.ts b/test/rules/SQS.test.ts index 4896a33977..e990716fa4 100644 --- a/test/rules/SQS.test.ts +++ b/test/rules/SQS.test.ts @@ -11,19 +11,26 @@ import { } from 'aws-cdk-lib/aws-iam'; import { Key } from 'aws-cdk-lib/aws-kms'; import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'; -import { CfnQueuePolicy, Queue, QueueEncryption } from 'aws-cdk-lib/aws-sqs'; +import { + CfnQueue, + CfnQueuePolicy, + Queue, + QueueEncryption, +} from 'aws-cdk-lib/aws-sqs'; import { Aspects, Stack } from 'aws-cdk-lib/core'; import { TestPack, TestType, validateStack } from './utils'; import { SQSQueueDLQ, SQSQueueSSE, SQSQueueSSLRequestsOnly, + SQSRedrivePolicy, } from '../../src/rules/sqs'; const testPack = new TestPack([ SQSQueueDLQ, SQSQueueSSE, SQSQueueSSLRequestsOnly, + SQSRedrivePolicy, ]); let stack: Stack; @@ -204,4 +211,43 @@ describe('Amazon Simple Queue Service (SQS)', () => { validateStack(stack, ruleId, TestType.COMPLIANCE); }); }); + + describe('SQSRedrivePolicy: SQS queues should have a redrive policy configured', () => { + const ruleId = 'SQSRedrivePolicy'; + + test('Noncompliance: L2 construct without redrive policy', () => { + new Queue(stack, 'QueueWithoutRedrive'); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Noncompliance: L1 construct without redrive policy', () => { + new CfnQueue(stack, 'L1QueueWithoutRedrive', {}); + validateStack(stack, ruleId, TestType.NON_COMPLIANCE); + }); + + test('Compliance: L1 construct with redrive policy', () => { + new CfnQueue(stack, 'L1QueueWithRedrive', { + redrivePolicy: { + deadLetterTargetArn: + 'arn:aws:sqs:us-east-1:123456789012:DeadLetterQueue', + maxReceiveCount: 3, + }, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + + test('Compliance: L2 construct with redrive policy', () => { + new Queue(stack, 'QueueWithRedrive', { + deadLetterQueue: { + queue: Queue.fromQueueArn( + stack, + 'Dlq2', + `arn:aws:sqs:${stack.region}:${stack.account}:foo2` + ), + maxReceiveCount: 3, + }, + }); + validateStack(stack, ruleId, TestType.COMPLIANCE); + }); + }); });